[Windows] Thread heartbeat automation injects a function_call_output without call_id and permanently breaks the thread on strict Responses API providers
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 125k
- Forks
- 19.4k
- PR merge metrics
- PR metrics pending
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).
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 tracing thread heartbeat automation from its injection path to the rollout JSONL, focusing on the response_item that becomes a function_call_output. Inspect the related thread_history_1.sqlite materialization behavior and the matching event_msg item_completed record. Done means heartbeats are persisted as user messages, or tool results include call_id, and subsequent strict Responses API requests succeed.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- api, backend-api-design
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100