Persisted function_call without matching function_call_output in resumed session
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.
5 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
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 (
CreatePullRequestpermission 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_callrejects a streamed tool item withRespondToModel, Codex currently records the original tool call and then appends afunction_call_outputwith an emptycall_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->FunctionCallOutputwith the samecall_idToolSearchCall->ToolSearchOutputwith the samecall_idCustomToolCall->CustomToolCallOutputwith the samecall_idWhy:
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_idResult: passed locally.
Note:
justis not installed in my local environment, so I used the scoped cargo test directly.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:
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.
Thank you for the detailed architectural feedback! This PR has been refined accordingly:
Key improvements:
ToolSearchCallargument parse failures, now returnsFunctionCallOutput(instead ofToolSearchOutput), ensuring the error message is properly conveyed to the modelsuccess: Some(false)to mark the failure status"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.rstests 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.
This is almost the exact persisted invariant Codex Rescue is designed to test: two durable
function_callrecords 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_patchran. 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.0a3codex-rescue sessionscodex-rescue doctor --latestIf 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