Start from a normal product scene. One client starts a task. Another client joins later and should see the current turn, streamed assistant output, a pending command approval, the latest diff, and token usage. If the page reloads, history should still reconstruct the important parts of the turn.
The source sequence is clearer than the final UI. The core protocol accepts typed submissions and emits typed events; app-server v2 converts those events into the notifications, requests, and list items that clients consume.
Keep three objects separate: Submission is the input work order; EventMsg is a runtime fact emitted by the runtime; ServerNotification is an app-server notification for clients.
Source scope. OpenAI's App Server article describes the product relationship: App Server sits between clients and the Codex harness, accepts client requests, and turns the harness event stream into client notifications. The public openai/codex source directly verifies protocol types, event types, app-server mapping code, and rollout reconstruction. Source links are pinned to one public snapshot.
This part follows five questions:
- What runtime parameters does a client send when it starts a turn?
- Why does core reduce interaction to one submission queue and one event queue?
- Which
EventMsgvariants describe lifecycle, content, tools, approvals, usage, and diff? - Why does app-server v2 translate core events instead of forwarding them verbatim?
- How can rollout be reduced back into turns after resume?
1. Start From What Clients Need To See
Protocols are hard to read when they begin as enum lists. The easier route is to ask what client-visible state must remain coherent while a Codex turn is running.
| Client Sees | Source Question | Boundary |
|---|---|---|
| A turn starts, completes, or is interrupted. | Who assigns the id and announces lifecycle changes? | Submission.id plus TurnStarted / TurnComplete. |
| Assistant text streams into the UI. | Is this a full message or an incremental event? | AgentMessage and AgentMessageContentDelta. |
| Commands, patches, MCP, and dynamic tools appear as progress items. | Does the UI infer tool progress, or does runtime emit structured facts? | ItemStarted / ItemCompleted and tool events. |
| A command or file change asks for approval. | Is this just a notification, or does it require a client response? | ExecApprovalRequest / ApplyPatchApprovalRequest. |
| History is visible after reload or resume. | What rebuilds the page state? | RolloutItem and ThreadHistoryBuilder. |
That is the shape of the problem: accept intent, preserve runtime facts, and let different clients consume those facts through their own view.
2. Input Side: A Typed Work Order
app-server v2 requests are not loose JSON blobs. The protocol macro generates
ClientRequest,
where each request has fixed params and response types.
The
turn/start
request uses TurnStartParams.
TurnStartParams
includes the obvious thread_id and input, but it also carries
runtime boundaries: client metadata, additional context, environment selection, cwd,
workspace roots, approval policy, sandbox policy, permissions, model, and service tier.
Starting a turn is therefore more like submitting a typed work order than sending chat text.
In turn_start_inner, app-server converts v2 input into TurnInput, collects settings overrides, and builds a TurnInputRequest. start_or_steer_turn lets core decide whether to start an idle thread, steer an active turn that can accept input, or decline the submission. The caller does not need to read state and race that decision.
A successful turn/start response means that core accepted the input; execution is not complete, and a new turn is not guaranteed. Starting returns a new turn id; steering returns the active turn id. Correlate later notifications with the response’s turn.id, rather than assuming that every JSON-RPC request or submission creates another turn.
The same ordering idea appears in Op::ThreadSettings. It does not start a
turn, but it uses the same submission queue so app-server can preserve caller order
between settings mutations and turn starts.
3. Core exposes a queue pair
The public thread object is now CodexThread. Runtime state lives in Session, while SessionIo holds the submission and event endpoints: tx_sub: Sender<Submission> and rx_event: Receiver<Event>. Separating the endpoints allows the session loop to terminate when all senders disappear and gives callers a shared shutdown completion future.
Ordinary submit allocates a unique id, wraps an Op in a Submission, and enqueues it. The envelope contains id, op, trace, and causal parent_turn_id / root_turn_id fields for multi-agent work. The client user-message id belongs to TurnInput::UserInput. submit_turn_input also includes a one-shot reply channel: it waits for core’s start, steer, or decline decision, not model completion. next_event independently consumes runtime events.
| Core Type | Role | Why It Matters |
|---|---|---|
Op |
What the runtime is being asked to do. | Includes user input, interrupt, approvals, compact, review, and more. |
Submission |
A work order with an id. | Gives later events a correlation id and carries trace and parent/root turn causality. |
Event |
A runtime fact with an id. | Lets clients attach a fact back to the turn submission. |
EventMsg |
The concrete kind of fact. | Separates lifecycle, content, tools, approvals, diff, token usage, compaction, and rollback. |
4. Output Side: EventMsg Is the Fact List
EventMsg
is a long tagged enum with #[serde(tag = "type", rename_all = "snake_case")].
That wire shape matters: runtime facts are structured variants, not informal strings.
| Category | Representative Variants | Meaning |
|---|---|---|
| Turn lifecycle | TurnStarted, TurnComplete, TurnAborted |
Announce when a turn starts, finishes, or is interrupted. |
| Model content | UserMessage, AgentMessage, AgentReasoning, deltas |
Carry full persisted messages and streaming increments. |
| Tools and file changes | ExecCommand*, PatchApply*, ItemStarted, ItemCompleted |
Represent command, patch, and tool lifecycles structurally. |
| Approval and interaction | ExecApprovalRequest, ApplyPatchApprovalRequest, RequestUserInput |
Pause where a human or client decision is needed. |
| State updates | TokenCount, TurnDiff, ContextCompacted, ThreadRolledBack |
Update usage, diff, compaction, and rollback state. |
The key distinction is that EventMsg is lower-level than the final UI.
A TurnDiffEvent carries the unified diff. A TokenCountEvent
carries token usage and rate-limit snapshots. Layout, grouping, folding, and emphasis
are decisions for client rendering.
5. app-server v2 converts core events for clients
At app-server, the protocol supports multiple clients. OpenAI's
App Server article
describes JSON-RPC between clients and the
Codex harness: it accepts client requests, turns them into core
operations, and converts the harness event stream into stable
client notifications. In source, app-server v2 defines those
notifications through
ServerNotification.
The method list includes turn/started, turn/completed,
item/started, item/completed,
turn/diff/updated, and thread/tokenUsage/updated.
The v2 view is built around Turn and ThreadItem. A turn
exposes status such as completed, interrupted, failed, and in progress. A
ThreadItem
turns user messages, assistant messages, plans, reasoning, and tool calls into
client list items.
The main translation point is
apply_bespoke_event_handling.
It receives core Event { id, msg }, matches on EventMsg,
and emits v2 notifications or server requests. Turn lifecycle events become turn
notifications; approval requests become server requests that need a client response;
token usage and diff have dedicated handlers.
Stateless one-to-one item mappings live in
item_event_to_server_notification.
That helper covers assistant message deltas, plan deltas, reasoning deltas,
item started/completed, exec output deltas, and similar direct mappings. Anything that
needs state checks, pending request cleanup, or legacy-event suppression stays in the
bespoke handler.
| Core Event | v2 Output | Reading Rule |
|---|---|---|
TurnStarted |
TurnStartedNotification |
Open a client-visible turn snapshot. |
AgentMessageContentDelta |
AgentMessageDeltaNotification |
Send streaming assistant text. |
ItemStarted / ItemCompleted |
ItemStartedNotification / ItemCompletedNotification |
Expose tool and item lifecycle to clients. |
ExecApprovalRequest / ApplyPatchApprovalRequest |
server request | Ask the client for a decision, then feed the answer back as another Op. |
TokenCount |
ThreadTokenUsageUpdatedNotification |
Convert runtime usage into thread-level client state. |
TurnDiff |
TurnDiffUpdatedNotification |
Expose the latest aggregated unified diff for the turn. |
This example starts a new turn and reaches a command requiring approval. Steering an existing turn does not open another turn; command events and text deltas may interleave:
turn/start request
-> EventMsg::TurnStarted -> turn/started notification
-> EventMsg::AgentMessageContentDelta -> item delta
-> EventMsg::ExecApprovalRequest -> server request
-> Op::ExecApproval -> core continues
-> EventMsg::ItemCompleted -> item/completed notification
-> EventMsg::TurnComplete -> turn/completed notification
The mapping code also suppresses or redirects events when that keeps the v2 view
coherent. For example, legacy ContextCompacted and some patch/exec events
are not blindly forwarded because v2 clients receive canonical items elsewhere.
6. Rollout Rebuilds Turns From Facts
The RolloutItem policy persists session metadata, response items, turn context, compaction and token-usage records, plus selected runtime events. Legacy history retains corresponding older message events; paginated history primarily stores completed TurnItem records. Intermediate deltas, approval requests, and ItemStarted are not all persisted. Historical ThreadRolledBack markers still participate in reconstruction, but the current request enum no longer exposes thread/rollback.
InitialHistory
exposes get_event_msgs() for resumed and forked histories by extracting
RolloutItem::EventMsg. Core
record_initial_history
applies rollout reconstruction and seeds token usage from the last persisted
TokenCount.
app-server has a dedicated reducer too:
build_turns_from_rollout_items
converts persisted rollout items into Turn values. Its
ThreadHistoryBuilder
handles persisted event messages, compacted items, response items, and turn context.
TurnStarted opens a turn; TurnComplete closes it.
At shape level, the reduction looks like this:
rollout items (simplified persisted records):
EventMsg(TurnStarted)
EventMsg(ItemCompleted(exec))
EventMsg(TurnComplete)
ThreadHistoryBuilder:
-> Turn { status: completed, items: [...] }
Resume is therefore a reduction over persisted runtime facts. It does not try to recreate one old frontend page; it rebuilds turns and items from durable evidence. Context recovery keeps the next model request usable; protocol/event recovery lets people and clients understand what already happened.
7. A Reading Checklist
| Type | Ask First | Better Reading |
|---|---|---|
TurnStartParams |
What boundaries does this turn carry? | Read input together with cwd, approvals, sandbox, model, permissions, and metadata. |
Op |
What runtime operation is being submitted? | Include approvals, interrupts, compact and review, not only user input. |
EventMsg |
Which runtime fact just happened? | Classify it as lifecycle, content, tool, approval, diff, usage, or recovery-related. |
ServerNotification |
Which client feature needs this notification? | Check whether bespoke handling transformed, suppressed, or split the core event. |
RolloutItem |
What can this record rebuild? | Treat it as replayable history material rather than rendered UI. |
The opening question now has a compact answer: clients can share one set of facts because Codex separates input, runtime events, client views, and replayable recovery records. The same reading rule applies to tools: once the model asks for a tool call, how does Codex route, execute, approve, and record it?
Sources
- Pinned openai/codex source snapshot
- OpenAI: Unlocking the Codex harness: how we built the App Server
- ClientRequest macro
- turn/start request definition
- TurnStartParams
- turn_start_inner mapping to Op::TurnInput
- Submission
- Op
- SessionIo submission/event endpoints
- submit / submit_with_id
- next_event
- Event / EventMsg
- ServerNotification macro
- apply_bespoke_event_handling
- item_event_to_server_notification
- Approval request mapping
- TokenCount mapping
- TurnDiff mapping
- RolloutItem
- build_turns_from_rollout_items
- ThreadHistoryBuilder