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:

  1. What runtime parameters does a client send when it starts a turn?
  2. Why does core reduce interaction to one submission queue and one event queue?
  3. Which EventMsg variants describe lifecycle, content, tools, approvals, usage, and diff?
  4. Why does app-server v2 translate core events instead of forwarding them verbatim?
  5. 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