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 projects them into JSON-RPC notifications; TUI projects them into history cells and status surfaces; rollout keeps the records needed for resume. Client projection is the boundary between those shapes.
Projection means one runtime fact changing owner and shape. It may be
an EventMsg, a ServerNotification, a
ThreadItem, a TUI history cell, or a rollout record. Ask who
owns the shape before asking how it appears on screen.
Evidence boundary. 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. They carry different ownership.
| 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 surfaces 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,
response_item: ResponseItem,
) {
self.record_conversation_items(turn_context, 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 Projects Facts Into Notifications
The app-server entry point is
apply_bespoke_event_handling.
It receives core events and maps them to app-server protocol surfaces.
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 Turn Status Is a Client Projection
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 Is a Request Surface
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 Projects 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 Settles Temporary Surfaces
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
Rollout persistence does not store every transient UI delta.
rollout policy
selects recordable response items and event messages. User messages,
assistant messages, reasoning, turn boundaries, compaction, rollback,
web search end, and image generation end survive. Many intermediate deltas,
approval requests, exec output deltas, and hook lifecycle events do not.
RolloutRecorder
queues canonical items, materializes and flushes rollout files, and loads
jsonl lines back into InitialHistory::Resumed. Core then uses
reconstruct_history_from_rollout
to rebuild model history, turn metadata, reference context, and rollback
effects. TUI replay renders safe items and marks them as replayed so live
side effects are not triggered.
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. |
Client projection protects readability and recovery. Core owns facts, app-server owns protocol boundaries, TUI owns live terminal experience, and rollout owns resume evidence. 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 projection
- 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