Repository navigation
Description
Summary
On Codex Desktop for macOS with a custom Kimi provider (wire_api = "responses"), every turn whose input is delivered as a system-injected standalone function_call_output fails immediately with HTTP 400. This is the same root cause as #41690 (DeepSeek) and #41799 (Azure), but the blast radius is wider and the fallout is worse:
- All injection paths are affected, not just cron automations: heartbeat automations, cron automations,
create_threaddelegation (<codex_delegation>), andsend_message_to_threadall deliver their input as afunction_call_outputwith nocall_id, and all fail identically. - The orphaned item poisons the thread permanently: it is persisted into the rollout history, so every subsequent turn of that thread — including plain user-typed messages — fails with the same error. The thread is bricked until the rollout file is manually repaired.
Kimi's Responses endpoint rejects the request with:
{"error":{"code":"invalid_request_error","message":"Invalid request: tool_call_id is not found","type":"invalid_request_error"}}
(note the empty tool_call_id — double space in the message).
Environment
- Codex Desktop (macOS):
26.825.51511(build7377) - OS: macOS 14.6.1, Apple Silicon (arm64)
- Provider config:
model_provider = "kimi"
model = "k3"
[model_providers.kimi]
name = "Kimi"
base_url = "https://api.kimi.com/coding/v1"
env_key = "[REDACTED]"
wire_api = "responses"Normal interactive turns (user-typed messages, normal tool calls) through this same provider work fine.
Steps to reproduce
- Configure the Kimi provider as above.
- Do any of the following:
- Create a heartbeat automation attached to a thread and wait for it to fire;
- Create a cron automation and wait for it to fire;
- From a working thread, call
create_threadorsend_message_to_thread.
- The new turn fails within ~5 seconds with
tool_call_id is not found. The model never receives the prompt.
Evidence
Rollout files (~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl) show the injected item has keys [id, internal_chat_message_metadata_passthrough, name, namespace, output, type] — no call_id:
{"type": "function_call_output", "id": "fco_01a07c84-bb4e-7ce2-b834-a3aa4dc6d3e9", "name": "automation_update", "namespace": "codex_app", "output": "<heartbeat>..."}{"type": "function_call_output", "id": "fco_01a07c76-1120-73e3-8ed5-3b7acf8f35ce", "name": "create_thread", "namespace": "codex_app", "output": "<codex_delegation>..."}In one affected thread, 150 function_call_output records were inspected: 149 normal tool outputs carry a call_id; exactly 1 — the heartbeat-injected automation_update — lacks it. That single record poisoned the whole thread: a heartbeat run at 09:31 failed, and two subsequent plain user messages in the same thread failed with the identical error.
Affected thread/session IDs (2026-09-07, Asia/Shanghai):
- Heartbeat run: thread
01a0746b-0025-77b0-aad5-aeb71f25b5ad, turn started 09:31, failed in ~4.7s - Delegated thread via
create_thread:01a07c76-1005-7973-965f-6dfcab2bb6a1, failed in ~6.9s - Cron automation run: thread
01a07c84-b8a5-7d60-aa53-5e8f2bedc9b4, failed in ~9.5s - Delegated message via
send_message_to_thread: sametool_call_iderror
Local workaround (confirms root cause)
Manually editing the rollout JSONL to convert the orphaned function_call_output into a plain user message item un-bricks the thread. This isolates the failure to the injection/serialization layer — scheduler, credentials, network, and prompt content are all unrelated.
Expected behavior
Standalone system-injected function_call_output items should never be sent to strict Responses providers without a valid call_id. Either synthesize a paired function_call + call_id, or normalize the item into a user/developer message at the provider boundary (as proposed in #42376). Additionally, a failed injection should not leave a malformed item in the persisted history that bricks all future turns of the thread.
Related
- Codex Desktop automation + DeepSeek Responses: standalone automation_update output lacks call_id (HTTP 400) #41690 — same root cause, DeepSeek, cron automations
- Codex Desktop macOS: Azure gpt-5.6-sol automations fail because bootstrap output has no call_id #41799 — same root cause, Azure, cron automations on macOS
- Scheduled tasks fail with custom Responses providers when standalone function_call_output omits call_id #42376 — same root cause with a proposed fix (normalize at the non-OpenAI provider boundary)
- responses: function_call_output can be emitted without call_id and fails strict upstreams (400) #42088 — dangling
function_call_outputbreaking resumed threads
No API keys or credentials are included in this report.
Activity
github-actions commentedon Sep 7, 2026
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
RuriLothlorien commentedon Sep 10, 2026
Adding a Windows + DeepSeek data point for the same root cause (@owenfuzhe) — a single thread heartbeat automation was enough to poison a thread permanently.
base_url = "https://api.deepseek.com",wire_api = "responses"), Codex Desktop 26.903.71938 / bundledcodex-cli 0.153.4, Windows x64.function_call_outputwithname = "automation_update",namespace = "codex_app",id = fco_...and nocall_id; every later turn then failed withFailed to deserialize the JSON body into the target type: input: missing field \call_id`` (400), and the thread could not recover on its own.message, then app restart) are in [Windows] Thread heartbeat automation injects a function_call_output without call_id and permanently breaks the thread on strict Responses API providers #44519.So: heartbeat automations are affected as well, and the failure is provider-independent in shape (
missing field call_id/tool_call_id is not found/invalid_promptdepending on the endpoint's validator).TimMCBen commentedon Sep 18, 2026
Adding a Windows data point on a newer build, plus one detail not yet reported here: every heartbeat run re-injects another
call_id-less record before the request goes out, so the thread accumulates one malformed item per scheduled fire — 18 in one thread and 40 in another over a single day.Environment
26.908.70816base_url = "https://api2.mcben.top/v1",wire_api = "responses"), modelskimi-k2.8/kimi-k3What happened
Three heartbeat automations (interval 30 min, attached to existing threads). Every single run followed the same sequence in the rollout JSONL (
~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl):task_startedresponse_itemof typefunction_call_outputwithname: "automation_update",namespace: "codex_app", anfco_...id, the<heartbeat>instructions payload — and nocall_idtask_completewith error after ~1.4 s, model never engaged:{"error":{"type":"invalid_request_error","message":"tool_call_id is not found"}}(note the double space — the relay converts the absent
call_idinto an emptytool_call_id, consistent with the proxy-boundary observation by @ZzkAItech above).The accumulation detail
Because the malformed injection is persisted into the thread history before the request is assembled, each failed run leaves its
<heartbeat>output in the rollout, and the next fire adds one more. Counts from the affected threads' rollouts:function_call_outputrecords, 18 withoutcall_id— allautomation_updateheartbeat injections, spaced exactly at the 30-min schedule.call_id— same pattern.keep_forked_rollout_itemcarried the poison into the child thread, matching thespawn.rsbehaviour described by @argszero.So on this provider the failure is deterministic from the very first heartbeat fire — even a perfectly clean thread gets poisoned by its own trigger injection before the request is sent, and no amount of rollout repair can keep a heartbeat thread healthy, since the next run re-poisons it.
Notes
function_call_outputwithid/name/namespace/outputand nocall_id) is what gets written on every fire; timestamps line up 1:1 with the schedule.01a0a46a-b29a-78d0-aa10-037aacaa7390(heartbeatali3-imder-iemocap),01a0a5e7-b041-70a3-9521-877d9edee4bb(heartbeatvideollama2-meld),01a0b20c-fa82-7672-8112-11259062a1e5(heartbeatvideollama2-meld-2, forked).turn/starttoolOutputproducer path pinned down by @argszero (turn_processor.rshardcodingcall_id: None). For strict providers, treatment option 1 from that analysis (rewrite to a plain message at request assembly, gated on a provider capability flag) is also what our manual rollout repair converged to: converting the record to a plain usermessageun-bricks the thread.No credentials, prompt contents, or server addresses included.