Start from an ordinary turn. The assistant is streaming text. A command starts, produces output, and completes. A permission prompt may need a user response. The turn finally moves from running to completed. Later, the same thread can be resumed and the transcript appears again.
The source does not treat those as separate UI tricks. Core emits and persists facts; app-server converts them into JSON-RPC notifications; TUI turns notifications into history cells and status lines; rollout keeps the records needed for resume. This article follows those concrete conversions from source event to visible state.
A conversion takes one runtime event and produces the data structure
needed by another module. The result may be
an EventMsg, a ServerNotification, a
ThreadItem, a TUI history cell, or a rollout record. First
find the converting module and its input and output types; then inspect
how the UI uses the result.
Source scope. This article only describes event types, mapping functions, notification structs, TUI handlers, and rollout reconstruction visible in the public openai/codex source. It does not infer private product UI internals.
1. Separate the Shapes First
The terms are easy to blur because several of them sound like history or events. Different modules use them for different jobs.
| Shape | Owner | Question answered |
|---|---|---|
EventMsg |
core protocol | What runtime fact just happened? |
TurnItem |
core item stream | Which response item is worth exposing as a turn item? |
ThreadItem |
app-server v2 protocol | How should clients receive transcript items? |
ServerNotification |
app-server JSON-RPC | Which notification should clients observe? |
| history cell | TUI | How should this fact render in terminal scrollback or live status? |
| rollout record | persistence / resume | Which fact must survive so the thread can be reconstructed? |
A message delta can update a live TUI stream immediately. The completed assistant message later becomes a stable item. Those two representations serve different lifetimes: one is live feedback, the other is replayable history.
2. Core Decides What Becomes a Turn Item
Client-visible items are narrowed before app-server rendering.
parse_turn_item
maps provider ResponseItem values into TurnItem
values for user messages, assistant messages, reasoning, web search, and
image generation. It also filters contextual fragments such as permission,
skills, and collaboration-mode payloads that are runtime context rather
than visible transcript.
Completed response items pass through
record_response_item_and_emit_turn_item:
record the response item, parse a turn item, then emit item-started and
item-completed events when a visible item exists.
record_conversation_items
updates in-memory history, persists rollout response items, and emits
RawResponseItem. Raw response, turn item, and UI item are
therefore separate layers.
pub(crate) async fn record_response_item_and_emit_turn_item(
&self,
turn_context: &TurnContext,
model_info: &ModelInfo,
response_item: ResponseItem,
) {
self.record_conversation_items(turn_context, model_info, std::slice::from_ref(&response_item))
.await;
if let Some(item) = parse_turn_item(&response_item) {
self.emit_turn_item_started(turn_context, &item).await;
self.emit_turn_item_completed(turn_context, item).await;
}
}
The useful lesson is the order: Codex records the provider response as conversation state first, then asks whether it can become a client-visible turn item. App-server and TUI therefore receive a narrowed runtime fact, not an arbitrary slice of the raw provider payload.
3. App-Server Converts Facts Into Notifications
The app-server entry point is
apply_bespoke_event_handling.
It receives core events and maps them to app-server protocol messages.
TurnStarted clears pending server requests, notes the running
turn, and sends ServerNotification::TurnStarted. Turn completion
aborts pending requests and emits a completion notification with status.
The wire names live in
ServerNotification:
turn/started, turn/completed,
item/started, item/completed, and delta notifications.
The v2 Turn shape carries items,
items_view, status, errors, and timing metadata.
3.1 App-Server Derives Turn Status From Events
Turn start sends an in-progress turn with unloaded items. Completion goes
through
handle_turn_complete,
which reads app-server turn summary state and chooses
Completed or Failed. Then
emit_turn_completed_with_status
sends the client-facing shape.
3.2 Item Lifecycle and Deltas Use Different Channels
Stateless one-to-one mappings use
item_event_to_server_notification.
It maps item lifecycle events to item notifications, agent text deltas to
AgentMessageDelta, and command output bytes to
CommandExecutionOutputDelta. Some paths add app-server state:
command begin/end events are deduplicated, and legacy command items are
suppressed for unified exec interactions so clients do not render duplicate
wait states.
3.3 Approval Uses a Request That Expects a Reply
Approval events become server requests because the client must answer.
ApplyPatchApprovalRequest and ExecApprovalRequest
send approval requests and await responses. They are not passive history
notifications.
4. TUI Converts Notifications Into Terminal State
TUI first routes app-server events by thread. For the active thread,
handle_thread_event_now
forwards notifications to ChatWidget::handle_server_notification.
That handler maps turn start to running state, turn completion to final
cleanup, item notifications to item-specific lifecycle handlers, and deltas
to streaming or active command cells.
4.1 Streaming Is a Temporary Tail
handle_streaming_delta
creates or updates a stream controller and starts commit animation. When
the final assistant item lands,
flush_answer_stream_with_separator
consolidates the streaming cells into one source-backed markdown cell so
resize and replay remain stable.
4.2 Item Type Chooses the History Cell
handle_item_started_notification
dispatches by ThreadItem: commands to command lifecycle, patches
to patch cells, MCP calls to MCP cells, web search to active search cells,
and so on. Completed items pass through
handle_thread_item,
which is shared by live handling and replay.
4.3 Turn Completion Finalizes Temporary UI State
on_task_started
resets turn flags, opens the running status, and shows the interrupt hint.
on_task_complete
flushes streaming and unified exec state, adds final separators when needed,
clears running commands, and decides whether to send notifications or queued
follow-up input.
5. Rollout Keeps Recovery Evidence
The rollout policy branches on history mode. Legacy stores older user-message, assistant-message, reasoning, and selected tool-completion events; paginated history primarily stores the complete TurnItem inside ItemCompleted. Both retain turn start, completion, interruption, usage, and settings-applied events. Intermediate deltas, approval requests, ItemStarted, and hook lifecycle events remain transient.
The RolloutRecorder first queues canonical items. persist materializes a deferred file; flush waits for queued writes. Sending a completion notification and confirming the writer has flushed are separate operations. A received turn/completed alone does not establish that every record is on disk. Loading parses JSONL records into InitialHistory::Resumed; core reconstructs surviving history and TUI replay renders safe items without live-only side effects.
For one assistant message, live deltas update a temporary TUI cell; completion produces a canonical item; paginated rollout stores that completed item; resume rebuilds the message without replaying every typing fragment. Historical ThreadRolledBack markers still require compatibility handling, but do not imply that the current app-server exposes a thread/rollback request.
6. Four Reading Rules
| Visible symptom | Ask first | Source layer |
|---|---|---|
| Text is streaming. | Is this a delta or a completed item? | AgentMessageDelta and stream consolidation. |
| A command card changes. | Is this lifecycle or output delta? | event mapping and command lifecycle. |
| An approval prompt opens. | Does the client need to answer? | ServerRequestPayload. |
| History reappears after resume. | Is this reconstructed state or a live event? | rollout reconstruction and TUI replay. |
These conversions keep the UI readable and history recoverable. Core produces facts, app-server emits protocol messages, TUI builds the live terminal experience, and rollout stores what resume needs. The next runtime layers, including plugins, MCP, skills, and subagents, still have to pass through the same question: who owns this fact, what can clients see, and what must survive for a future resume?
Sources
- Pinned openai/codex source snapshot
- EventMsg turn, item, tool, and delta events
- Core TurnItem
- parse_turn_item and contextual message filtering
- record_conversation_items
- record_response_item_and_emit_turn_item
- app-server turn event handling
- approval request conversion
- command execution notification handling
- emit_turn_completed_with_status
- handle_turn_complete
- ServerNotification wire names
- item_event_to_server_notification
- item and delta event mapping
- ThreadItem variants
- CoreTurnItem to ThreadItem conversion
- Turn and TurnItemsView
- TUI handle_app_server_event
- TUI handle_thread_event_now
- ChatWidget handle_server_notification
- ChatWidget item notification dispatch
- flush_answer_stream_with_separator
- handle_streaming_delta
- command start and output delta
- command completed
- on_task_started
- on_task_complete
- TUI replay_thread_turns and handle_thread_item
- rollout persistence policy
- RolloutRecorder write and load
- rollout reconstruction