Persisted function_call without matching function_call_output in resumed session

Open 💬 5 comments Opened Jun 24, 2026 by Vxider
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

Summary

In a long Codex session, the local rollout persisted assistant function_call items for apply_patch, but no matching function_call_output items were persisted. Every later resumed turn logs:

Function call output is missing for call id: call_gvk6lf6jpooc73szk0ezst3d
Function call output is missing for call id: call_5hln5qbxp2yt3bugadxrtr3x

This leaves the session transcript in an inconsistent Responses-style state and appears to make subsequent long-running turns less stable.

Environment

  • Codex CLI version: 0.142.0
  • OS: Ubuntu 24.04
  • Model provider: custom Responses-compatible provider at http://localhost:8317/v1
  • Model: gpt-5.4
  • Session source: Codex TUI

Evidence

The local rollout contains an assistant tool call:

{
  "type": "response_item",
  "payload": {
    "type": "function_call",
    "id": "fc_call_gvk6lf6jpooc73szk0ezst3d",
    "name": "apply_patch",
    "call_id": "call_gvk6lf6jpooc73szk0ezst3d",
    "arguments": "{\"mode\":\"patch\",\"path\":\"/home/.../src/pages/PptWorkspacePage.tsx\",\"command\":\"diff --git ...\"}"
  }
}

And later another one:

{
  "type": "response_item",
  "payload": {
    "type": "function_call",
    "id": "fc_call_5hln5qbxp2yt3bugadxrtr3x",
    "name": "apply_patch",
    "call_id": "call_5hln5qbxp2yt3bugadxrtr3x",
    "arguments": "{\"mode\":\"patch\",\"path\":\"/home/.../src/pages/PptWorkspacePage.tsx\",\"command\":\"diff --git ...\"}"
  }
}

But the rollout has no corresponding function_call_output item for either call id.

Every later turn logs:

codex_core::context_manager::normalize: Function call output is missing for call id: call_gvk6lf6jpooc73szk0ezst3d
codex_core::context_manager::normalize: Function call output is missing for call id: call_5hln5qbxp2yt3bugadxrtr3x

Expected behavior

Once Codex accepts and persists an assistant function_call, it should always persist a matching function_call_output, even if:

  • tool execution fails,
  • argument parsing fails,
  • the tool schema is invalid,
  • the tool does not exist,
  • the local executor aborts before producing normal output.

For example, Codex could persist a synthetic failed tool output:

Tool execution failed before producing output: invalid apply_patch arguments.

Alternatively, when preparing a resumed turn, Codex should repair or omit orphan function calls rather than carrying an inconsistent transcript forward indefinitely.

Actual behavior

The orphan tool calls remain in the session history. Later turns repeatedly log missing function call outputs during context normalization.

Notes

The suspicious part is that the model emitted apply_patch as a JSON function call with fields like mode, path, and command, while the local apply_patch tool appears to expect a freeform patch. Even if those arguments are invalid, Codex should record a tool error output instead of leaving the call without output.

I am not attaching the full rollout because it contains a large amount of private project context, but the issue is reproducible from the transcript invariant above: a persisted assistant tool call without a matching tool output.

View original on GitHub ↗

5 Comments

github-actions[bot] contributor · 2 months ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #29448
  • #29447

Powered by Codex Action

Vxider · 2 months ago

I prepared a small candidate patch for one concrete transcript-integrity edge case, but this repository currently does not allow my account to create a PR directly (CreatePullRequest permission denied). Sharing the branch here in case maintainers want to invite a PR or cherry-pick it.

Branch:
https://github.com/Vxider/codex/tree/fix/tool-parse-error-call-output

Commit:
https://github.com/Vxider/codex/commit/af10884704

What it changes:

When ToolRouter::build_tool_call rejects a streamed tool item with RespondToModel, Codex currently records the original tool call and then appends a function_call_output with an empty call_id. That leaves the transcript without an output matching the original tool call id.

The patch changes that error path to construct a failed output item that matches the original tool item:

  • FunctionCall -> FunctionCallOutput with the same call_id
  • ToolSearchCall -> ToolSearchOutput with the same call_id
  • CustomToolCall -> CustomToolCallOutput with the same call_id

Why:

Tool-call transcripts should preserve the invariant that a model-visible tool call has a corresponding model-visible output for the same call id, even when argument parsing or routing fails before the tool can execute.

Validation:

  • cargo test -p codex-core handle_output_item_done_records_failed_tool_output_with_matching_call_id

Result: passed locally.

Note: just is not installed in my local environment, so I used the scoped cargo test directly.

rpelevin · 2 months ago

I would make this a transcript-integrity invariant, not just a recovery warning during normalization.

The invariant I would pin is: once a function_call is accepted into persisted history, the resumed transcript must contain exactly one terminal counterpart for that call_id before the next provider request is built.

That counterpart can be a normal function_call_output, a synthetic tool-error output, or a repaired terminal record during resume, but it should not be absent while the orphan function_call remains visible to the model.

Useful regression shape:

  1. Persist an assistant function_call for a local tool whose arguments cannot be parsed by the local executor.
  2. Force the executor path to fail before normal tool output is produced.
  3. Resume the same session.
  4. Assert context normalization does not carry a bare function_call forward.
  5. Assert the repaired transcript has exactly one terminal item bound to the original call_id.
  6. Assert the terminal item preserves the tool name, call_id, failure reason, and no-execution status.

That keeps resume deterministic: the model sees a closed tool turn, observability sees why execution did not happen, and later turns do not inherit an impossible Responses-style transcript.

Boundary: architecture and regression-test feedback only; no claim about using this project or running its code.

Vxider · 2 months ago

Thank you for the detailed architectural feedback! This PR has been refined accordingly:

Key improvements:

  1. For ToolSearchCall argument parse failures, now returns FunctionCallOutput (instead of ToolSearchOutput), ensuring the error message is properly conveyed to the model
  2. Output includes success: Some(false) to mark the failure status
  3. Error message preserves the specific parse failure reason (e.g., "failed to parse tool_search arguments: ...")

Mapping to suggested regression test shape:

| Test shape | Implementation |
|------------|----------------|
| 1. Persist function_call with unparseable arguments | ✅ Test uses {"query": 42} to trigger type error |
| 2. Force executor to fail before normal output | ✅ Failure occurs at parse stage |
| 3. Resume same session | ⚠️ Test covers execution-time fix; normalization tests already exist |
| 4. Assert normalization doesn't carry bare function_call | ✅ Existing normalize.rs tests cover this |
| 5. Assert exactly one terminal item bound to original call_id | ✅ Test verifies |
| 6. Assert terminal preserves failure reason | ✅ Test verifies |

This PR generates the correct output at execution time, intervening earlier than the normalization phase and avoiding the generic "aborted" message.

shleder · 14 days ago

This is almost the exact persisted invariant Codex Rescue is designed to test: two durable function_call records with no matching outputs, followed by repeated missing-output warnings on every resume.

The tool is local-first and should classify the calls as unfinished without guessing whether either apply_patch ran. It never fabricates outputs, replays calls, or modifies the original rollout; salvage is bounded and verification remains fail-closed.

If you still have that session, could you try:

pipx install codex-rescue==0.1.0a3
codex-rescue sessions
codex-rescue doctor --latest

If doctor finds the boundary, we can then test a forked salvage. Sanitized output is enough—please don’t share the rollout, arguments, repository files, prompts, paths, databases, or secrets.

https://github.com/shleder/codex-rescue