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 boundary is cleaner than the UI surface. There is a layer of runtime facts, and there are client projections built from those facts. The core protocol accepts typed submissions and emits typed events; app-server v2 turns those events into the shapes clients actually 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.

Evidence boundary. OpenAI's App Server article provides the product-level boundary: 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 boundaries 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 maps v2 input into core input, resolves cwd and environment selections, builds thread-settings overrides, creates Op::UserInput, and submits it through submit_user_input_with_client_user_message_id.

The important line: turn/start returns the core submission id as turn_id. That gives later events a stable id to point back to the 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 Boundary: A Queue Pair

Once app-server has an Op, the core boundary becomes very small. The Codex struct is documented as a queue pair: send submissions, receive events. Its fields are the two channels: tx_sub: Sender<Submission> and rx_event: Receiver<Event>.

submit generates a UUID, wraps the Op in Submission, and sends it to tx_sub. next_event reads the next Event from rx_event. The event id is documented as correlated with the submission id.

Core Type Role Why It Matters
Op What the runtime is being asked to do. Includes user input, interrupt, approvals, compact, rollback, review, and more.
Submission A work order with an id. Gives later events a correlation id and carries trace/client message metadata.
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 belong to the projection layer.

5. app-server v2 Projects Facts Into Client Views

At app-server, the protocol becomes a multi-client surface. OpenAI's App Server article frames this layer as the JSON-RPC boundary between clients and the Codex harness: it accepts client requests, turns them into core operations, and projects 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 projections 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 Project 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.

Connected end to end, a command that needs approval roughly crosses the boundary like this:

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 projection layer 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

Realtime notifications explain what connected clients see. Reload and resume require a durable source. Protocol RolloutItem can store SessionMeta, ResponseItem, Compacted, TurnContext, and EventMsg. It stores rebuildable material, not a screen snapshot.

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:
  EventMsg(TurnStarted)
  EventMsg(ItemStarted(exec))
  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, rollback, 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 view needs this projection? 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