Repository navigation
Description
@etraut-openai — filing this because it a) bricks a thread permanently and b) is a protocol-level issue (Codex emits an item that fails strict Responses API schema validation).
What version of Codex App are you using (From "About Codex" dialog)?
26.903.71938 (Windows x64). Bundled Codex core: codex-cli 0.153.4.
What subscription do you have?
None — this install uses a third-party model provider via the Responses wire API (DeepSeek, base_url = "https://api.deepseek.com", wire_api = "responses").
What platform is your computer?
Windows 11 x64 (Microsoft Windows NT 10.0.26200.0).
What issue are you seeing?
A scheduled thread heartbeat automation wrote an invalid item into the thread history. After the first heartbeat fired, every subsequent request in that thread failed with a 400 and the thread became permanently unusable:
{"error":{"message":"Failed to deserialize the JSON body into the target type: input: missing field `call_id` at line 1 column 3173902","type":"invalid_request_error","param":null,"code":"invalid_request_error"}}
The malformed item in the rollout is a response_item of type function_call_output that has no call_id, even though its name is automation_update and its output is the heartbeat payload:
{"type":"function_call_output","id":"fco_01a08b47-193c-7ca3-8d64-f97435ad21e9","name":"automation_update","namespace":"codex_app","output":"<heartbeat>\r\n <automation_id>10</automation_id>\r\n ...\r\n</heartbeat>\r\n","internal_chat_message_metadata_passthrough":{"turn_id":"01a08b47-1934-7b72-8f31-93217d57f8bd","create_time":1789043087.6964047}}The heartbeat spec says the heartbeat should arrive as a user message wrapped in a <heartbeat> XML tag, not as a tool result.
What steps can reproduce the bug?
- In a thread, create a heartbeat automation targeting that thread (
kind="heartbeat",destination="thread",targetThreadId=<thread>, short interval, e.g.FREQ=MINUTELY;INTERVAL=1). - Wait for the first heartbeat injection.
- Send any message in that thread, using a third-party provider over the Responses wire API that validates the request schema strictly (here: DeepSeek).
What is the expected behavior?
The heartbeat is delivered as a user message (per the heartbeat spec), and the thread keeps working.
What actually happens
- The heartbeat is persisted as a
function_call_outputfor theautomation_updatetool (namespacecodex_app) withoutcall_id, plus a matchingevent_msgitem_completedwithtype: FunctionCallOutput. - Because the malformed item stays in history, every later turn replays it and is rejected with the same error (the reported column grows with history size:
3173902then3174148). - The thread can never recover on its own: requests never reach the model, so no auto-compaction can prune the item. Only a new thread works.
Timeline (UTC, 2026-09-10)
| time | event |
|---|---|
| 12:14:35 | automation created (function_call automation_update, call_id=call_00_LAK9DbjniBIKDdDlwLTX1933) |
| 12:24:47 | first heartbeat injected -> malformed function_call_output appended to the thread rollout (rollout-...01a014f9-....jsonl, line 1936) |
| 12:24:50 | turn fails: missing field call_id ... column 3173902 |
| 12:27:24 | same failure, column 3174148 |
| 12:44:26 | same failure (thread still broken) |
Impact
- The affected thread is bricked: no message can be sent until the history is repaired manually.
- Any user who attaches a heartbeat automation to a thread can hit this; on providers that don't validate strictly it may silently pass, so the failure mode is provider-dependent.
Workaround
- Locate the thread rollout JSONL and find the
function_call_outputitem that lackscall_id. - Replace it with a regular
messageitem (roleuser) that carries the heartbeat text — keeping the byte length identical (or, after editing, delete the thread's rows fromthread_history_1.sqliteso it re-materializes). - Restart the Codex app. In our case the thread resumed normally afterwards (verified with successful turns a minute later).
Suggested fix
- Deliver heartbeats as ordinary user messages (as the heartbeat spec advertises), or
- if a tool-result item must be written, include the originating
call_id, or - don't emit any
function_call_outputfor non-tool events.
Related: #44378 (a different history item that strict third-party Responses providers also reject).
Activity
github-actions commentedon Sep 10, 2026
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
invalid_prompton OpenRouter provider (Responses wire API) #43740Powered by Codex Action
RuriLothlorien commentedon Sep 10, 2026
Reviewed the duplicates the bot flagged (#43515, #43740, #43944, #43296) — confirmed they are the same root cause family: the local delivery paths persist injected input as a
function_call_outputwithoutcall_id, which strict Responses-wire providers reject.Keeping this issue open on purpose: it is the Windows + DeepSeek instance with the most precise artifacts.
call_id) and its rollout line +item_completedsibling.missing field \call_id``), including the growing column offset.messagecarrying the heartbeat text), then restart the app — the thread resumed normally.#43515 looks like the best consolidation target for the general fix (it also links #41690 DeepSeek and #41799 Azure). Happy to close this one as a duplicate if maintainers prefer a single tracker — just say so and I'll close it with a pointer there.
@etraut-openai
bjdx8j4zm5-source commentedon Sep 12, 2026
Workaround (verified): deliver the trigger the way it was delivered before
e56e4922ebSame setup as reported here (Windows, Codex Desktop,
wire_api = "responses", DeepSeek).We hit both halves of the failure: the automation never completed a single run, and the
target thread became permanently unusable — every later user message returned the same
HTTP 400.
As @argszero traced above, the trigger used to arrive as a user message; since
e56e4922ebit arrives as a standalonefunction_call_outputwith nocall_id. And as@njwangcn pointed out, populating
call_idalone is not a fix either — there is nomatching
function_callto pair it with.So rather than trying to make the item valid, add a local passthrough proxy that rewrites
it back into the old form before it reaches the provider:
Everything else — tool calls, reasoning items, streaming — is forwarded unchanged.
Verified end to end:
function_call_output, sent directly toapi.deepseek.commissing field 'call_id'response.completedcodex execrun with the shim asbase_urlGET /modelsand a non-streamingPOST /responsesOutcome: the automation actually runs instead of dying on its first trigger, and no
malformed item is ever persisted — so the "permanently bricked thread" failure mode
disappears.
For threads that are already bricked, the same repo ships a repair script that removes
the orphan outputs from both the rollout JSONL and
thread_history_1.sqlite(
thread_items), realignsthread_history_projection_state, and backs everything upbefore touching it (dry-run by default). Restart Codex afterwards.
Code + write-up: https://github.com/bjdx8j4zm5-source/codex-strict-responses-fix
Caveat: this is a client-side workaround, not an app-server fix. The proper fix is either
attaching a real
call_idwith a matchingfunction_call, or going back touser-message delivery for injected triggers.
中文版
同样的环境(Windows + Codex 桌面版 +
wire_api = "responses"+ DeepSeek)。我们遇到了这个问题的两半:自动化一次都没成功运行过,而且目标会话被永久搞坏——之后每条消息都返回同样的
HTTP 400。
正如 @argszero 上面定位的:触发内容以前是以 user 消息下发的;
e56e4922eb之后改成独立的
function_call_output,且不带call_id。而 @njwangcn 说得对:只补call_id也不行——它没有配对的
function_call。所以不去"修正"这条记录,而是加一个本地透传代理,把它改回旧格式再发给服务商:
其余内容(工具调用、reasoning、流式响应)原样透传。
端到端验证:
function_call_output的请求,直连api.deepseek.commissing field 'call_id'response.completedcodex exec以代理为base_urlGET /models与非流式POST /responses结果:自动化能真正跑起来(而不是在第一次触发就死掉),并且不会再写入坏记录,因此
"会话永久损坏"这个失败模式消失。
对于已经坏掉的会话,同一个仓库里有修复脚本:从 rollout JSONL 和
thread_history_1.sqlite(thread_items)里删除孤儿输出、校正thread_history_projection_state偏移,改动前自动备份(默认 dry-run),修完重启Codex 即可。
代码与说明:https://github.com/bjdx8j4zm5-source/codex-strict-responses-fix
说明:这是客户端绕过方案,不是 app-server 的修复。官方正确修法是:补上真实的
call_id并配对
function_call,或让注入的触发内容改回 user 消息投递。