Skip to content

[Windows] Thread heartbeat automation injects a function_call_output without call_id and permanently breaks the thread on strict Responses API providers #44519

Open

Description

@RuriLothlorien

@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?

  1. In a thread, create a heartbeat automation targeting that thread (kind="heartbeat", destination="thread", targetThreadId=<thread>, short interval, e.g. FREQ=MINUTELY;INTERVAL=1).
  2. Wait for the first heartbeat injection.
  3. 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_output for the automation_update tool (namespace codex_app) without call_id, plus a matching event_msg item_completed with type: 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: 3173902 then 3174148).
  • 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

  1. Locate the thread rollout JSONL and find the function_call_output item that lacks call_id.
  2. Replace it with a regular message item (role user) that carries the heartbeat text — keeping the byte length identical (or, after editing, delete the thread's rows from thread_history_1.sqlite so it re-materializes).
  3. 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_output for non-tool events.

Related: #44378 (a different history item that strict third-party Responses providers also reject).

Activity

  1. added
    bugSomething isn't working
    appIssues related to the Codex desktop app
    windows-osIssues related to Codex on Windows systems
    custom-modelIssues related to custom model providers (including local models)
    sessionIssues involving session (thread) management, resuming, forking, naming, archiving
    on Sep 10, 2026
  2. RuriLothlorien commented on Sep 10, 2026

    @RuriLothlorien
    Author

    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_output without call_id, which strict Responses-wire providers reject.

    Keeping this issue open on purpose: it is the Windows + DeepSeek instance with the most precise artifacts.

    • Exact malformed rollout item (type / id / name=automation_update / namespace=codex_app / missing call_id) and its rollout line + item_completed sibling.
    • Timeline from automation creation -> first heartbeat injection -> first 400 (missing field \call_id``), including the growing column offset.
    • A workaround that is verified on this machine: patch that one rollout line in place with identical byte length (replace the tool output with a plain user message carrying 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

  3. bjdx8j4zm5-source commented on Sep 12, 2026

    @bjdx8j4zm5-source

    Workaround (verified): deliver the trigger the way it was delivered before e56e4922eb

    Same 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
    e56e4922eb it arrives as a standalone function_call_output with no call_id. And as
    @njwangcn pointed out, populating call_id alone is not a fix either — there is no
    matching function_call to 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:

    # every function_call_output without call_id -> plain user message with the same text
    {"type": "function_call_output", "id": "fco_…", "name": "automation_update",
     "namespace": "codex_app", "output": "<heartbeat>…</heartbeat>"}
    # becomes
    {"type": "message", "role": "user",
     "content": [{"type": "input_text", "text": "<heartbeat>…</heartbeat>"}]}

    Everything else — tool calls, reasoning items, streaming — is forwarded unchanged.

    [model_providers.deepseek]
    base_url = "http://127.0.0.1:19100/"   # local shim -> api.deepseek.com
    wire_api = "responses"

    Verified end to end:

    test result
    request containing an orphan function_call_output, sent directly to api.deepseek.com 400 missing field 'call_id'
    identical request through the shim 200, 27 SSE events, clean response.completed
    real codex exec run with the shim as base_url 200, normal model reply
    GET /models and a non-streaming POST /responses 200

    Outcome: 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), realigns thread_history_projection_state, and backs everything up
    before 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_id with a matching function_call, or going back to
    user-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。

    所以不去"修正"这条记录,而是加一个本地透传代理,把它改回旧格式再发给服务商:

    # 每个缺 call_id 的 function_call_output -> 内容相同的普通 user 消息
    {"type": "function_call_output", "id": "fco_…", "name": "automation_update",
     "namespace": "codex_app", "output": "<heartbeat>…</heartbeat>"}
    # 变成
    {"type": "message", "role": "user",
     "content": [{"type": "input_text", "text": "<heartbeat>…</heartbeat>"}]}

    其余内容(工具调用、reasoning、流式响应)原样透传。

    [model_providers.deepseek]
    base_url = "http://127.0.0.1:19100/"   # 本地代理 -> api.deepseek.com
    wire_api = "responses"

    端到端验证:

    测试 结果
    含孤儿 function_call_output 的请求,直连 api.deepseek.com 400 missing field 'call_id'
    同一请求走代理 200,27 个 SSE 事件,正常 response.completed
    真实 codex exec 以代理为 base_url 200,模型正常回复
    GET /models 与非流式 POST /responses 200

    结果:自动化能真正跑起来(而不是在第一次触发就死掉),并且不会再写入坏记录,因此
    "会话永久损坏"这个失败模式消失。

    对于已经坏掉的会话,同一个仓库里有修复脚本:从 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 消息投递。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    appIssues related to the Codex desktop appautomationsbugSomething isn't workingcustom-modelIssues related to custom model providers (including local models)sessionIssues involving session (thread) management, resuming, forking, naming, archivingwindows-osIssues related to Codex on Windows systems

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions