Desktop/app-server history replay omits persisted commandExecution items
Open 💬 2 comments Opened Jun 14, 2026 by VincentAdamNemessisX
Summary
Codex Desktop/app-server history replay appears to omit persisted command execution items after a session is reloaded. The raw rollout JSONL still contains the command tool calls and outputs, but thread/read with includeTurns: true returns no commandExecution items for the same thread.
Environment
- Codex CLI:
codex-cli 0.133.0 - Codex Desktop / app-server protocol:
thread/readwithincludeTurns: true - Platform: macOS
Evidence
- Raw rollout JSONL contains persisted command execution data, including
response_itempayloads withtype: "function_call"andname: "exec_command", matchingfunction_call_outputpayloads, and in some sessionsevent_msgpayloads withtype: "exec_command_end". - Across a local rollout corpus, the raw files include 47,534
exec_commandfunction calls, 56,291 function call outputs, and 5,603exec_command_endevents. - For one sampled thread, raw JSONL contained thousands of tool/function rows. The SQLite thread index pointed to the correct rollout path. However
thread/read includeTurns=truereturned 130 turns with item counts such asfileChange: 250,mcpToolCall: 53,agentMessage: 757, andcommandExecution: 0. - Additional sampled sessions with only legacy
response_item.function_call(name=exec_command)rows and sessions with newerevent_msg.exec_command_endrows also replayed withcommandExecution: 0. - This makes command execution details disappear from the Desktop UI after reload/switch even though the underlying rollout JSONL is still intact.
Expected behavior
History replay should reconstruct command execution UI items, or an equivalent expandable tool-call item, from persisted rollout rows such as:
{"type":"response_item","payload":{"type":"function_call","name":"exec_command","call_id":"...","arguments":"{...}"}}
{"type":"response_item","payload":{"type":"function_call_output","call_id":"...","output":"..."}}
{"type":"event_msg","payload":{"type":"exec_command_end","call_id":"...","command":["/bin/zsh","-lc","..."],"cwd":"...","exit_code":0,"status":"completed"}}
Actual behavior
After reload/history replay, command execution details are missing from the returned thread items and from the Desktop UI. Other history items such as assistant messages, file changes, and some MCP/tool items can still appear.
Reproduction outline
- Create or identify a Codex session that has shell command executions.
- Confirm the raw rollout JSONL contains
exec_commandfunction calls and matching outputs, and/orexec_command_endevents. - Start app-server and call
thread/readwithincludeTurns: truefor that thread. - Count returned turn items. The raw rollout has command execution data, but the replayed turns contain zero
commandExecutionitems.
Notes
- This report intentionally avoids including local paths, credentials, command output, or private session content.
- The problem looks like a history replay/normalization issue rather than physical JSONL loss: the raw rollout files remain intact and the SQLite
threads.rollout_pathvalues point to the correct files.
2 Comments
I took a look at the current replay reducer. The missing-command-history behavior seems to split into two cases.
For newer rollout rows that contain
event_msgvalues, currentmainalready has a replay path:thread/read includeTurnsloads rollout history and callsbuild_api_turns_from_rollout_items(...)ThreadHistoryBuilder::handle_event(...)handlesEventMsg::ExecCommandBeginandEventMsg::ExecCommandEndhandle_exec_command_end(...)callsbuild_command_execution_end_item(...), which maps the core event intoThreadItem::CommandExecutionwithaggregated_output,exit_code, andduration_msapp-server-protocol/src/protocol/thread_history.rsasserting that anExecCommandEndEventreplays into a completedCommandExecutionitemThe gap I do see is the legacy-only shape mentioned in the report: rollout rows that have only
response_item.function_call(name = "exec_command")plus matchingresponse_item.function_call_output, without the newerEventMsg::ExecCommandBegin/Endrecords. In currentThreadHistoryBuilder::handle_response_item(...), response-item replay only handles userMessagerows that parse as hook prompts. Non-message response items, includingfunction_callandfunction_call_output, return early and do not contribute anyThreadItem.That explains why older sessions with only persisted Responses API tool-call rows can replay with
commandExecution: 0even though the raw JSONL still contains command calls and outputs.A minimal fix path would probably be:
EventMsg::ExecCommand*replay as the canonical path for newer rollouts.ThreadHistoryBuilderthat pairsResponseItem::FunctionCall { name: "exec_command", call_id, arguments, ... }withResponseItem::FunctionCallOutput { call_id, output }.ThreadItem::CommandExecutionfrom those paired rows, using the parsed command/cwd from the arguments when available and the function-call output asaggregated_output.ExecCommandEndevent rows exist for the samecall_id; the event row should win because it carries richer status, cwd, parsed command actions, exit code, and duration.A focused regression test could build rollout items containing only the legacy
response_itempair for anexec_commandcall and assertbuild_turns_from_rollout_items(...)returns oneThreadItem::CommandExecution. A second test should include both the legacy pair and anExecCommandEndEventfor the samecall_idand assert only the event-derived command item survives.Per
docs/contributing.md, I am not opening an unsolicited PR, but this looks like a contained replay-normalization fix if maintainers want it.Okay, thanks for your reply.