OpenAI Codex · Code Mode

Before You Stuff Context, Give It a Type

Approved prefixes, the workspace, AGENTS.md, an interrupted turn — all tell the model a fact it cannot see for itself. Codex first mints a type for each injection; the type knows its own role, marker, and body.

Course goalAfter this lesson, you can explain three things: why every stretch of text stuffed into model context must first become a type; how marked and unmarked differ, and what claims them back after compact or restore; and which fields sort a message to the model, and where the order comes from.
Play first · Flip the switches; the envelope holds those blocks
Same-turn input: five fragment switches, watch how the messages are assembled
On
Flip a switch and the left envelope re-sorts at once. Hit Play: watch each block get asked for role and marker, then land in one of three messages.
Envelope to the model0 msgs · 0 blocks
Origin cardAsk the type first
Logic trail · each animation step maps to a source span
  1. Read role: user or developerfragment.rs L15
  2. Ask whether it needs its own messagefragment.rs L18
  3. The instance wants markers; use them to renderfragment.rs L22
  4. Empty markers emit only the body and never matchfragment.rs L41
  5. Window identity is pushed into the separate bin before the loopsession/mod.rs L3661
  6. Other fragments sort by role and markersession/mod.rs L3677
  7. On the user side, the registry .any() claims it backcontextual_user_message.rs L18
  8. On the developer side, the prefix table claims it backevent_mapping.rs L40
Turn switches on or off and see which blocks make up what the model finally gets. Hit Play to watch the sort.
Where the makeup comes fromMergeable developer spans fold into one message; ones that must stand alone each get their own; user spans fold into one more. Order lives on the type fields. Whether a word appears in the string does not help.
Can it be claimed laterMarked ones come back via begin/end markers. Unmarked ones default to unclaimable; the developer side patches some of that with a prefix table.
Why mint a type firstThe assembly door only takes fragments that already implement the trait. A casually format!ed tag cannot enter, and the matcher list will not recognize an unregistered tag.
Teaching sketch: Switch combos and body wording are course-adapted; sort order follows build_initial_context_with_world_state. Line numbers on the logic trail match openai/codex commit 4f39251a01.
Idea 1 · An injection first becomes a type
What problem it solves

You’ve added this kind of feature to an agent: the user just approved npm *, and next turn the model still asks whether to run npm test. Or compact just finished and the model suddenly forgets where the workspace is, or whether the sandbox is read-only. Or you fork a session: the old AGENTS.md is still there, a new directory hint stacks on top, and the model sees two sets of rules fighting.

All three share one kind of operation: at runtime, a stretch of text is stuffed into model context. Approved prefixes, workspace, permission profile, an interrupted turn, the current UTC time — all tell the model a fact it cannot see. A single format! can write it.

The trouble is afterwards. Once that text is in history, whether compact keeps it, restore claims it, the UI hides it, or world state treats it as something the model already knows — all depend on one thing: you can still claim it back from plain text. Write another startsWith in each place and in a few weeks they drift. Unknown tags also leak into user-message parsing.

What the idea is

Codex folds each injection into a type that implements ContextualUserFragment. The trait is not in codex-core; it lives in a separate crate, codex-context-fragments. Every impl must answer at once: does this enter the Responses API as user or developer, what are the begin/end markers, how is the body written, and does it need its own message.

render() concatenates begin marker, body, and end marker with no separator. Whitespace and newlines belong to the body. Empty markers emit only the body. into() folds the render into a ResponseItem::Message whose content is a single InputText. The injection ends as a protocol object.

codex-rs/context-fragments/src/fragment.rslines 38–46
    fn render(&self) -> String {
        let (start_marker, end_marker) = self.markers();
        let body = self.body();
        if start_marker.is_empty() && end_marker.is_empty() {
            return body;
        }

        format!("{start_marker}{body}{end_marker}")
    }
Source snapshot note: Based on the local openai/codex repo; checked against codex-rs/context-fragments/src/fragment.rs, commit 4f39251a01, checked on 2026-08-22. The code block keeps the source as-is. This is the whole rule for a type folding itself into injectable text.

markers() takes self — render walks the instance. type_markers() does not — claim walks the type. With only history text and no original object, you can still ask whether this stretch looks like a given fragment. dyn ContextualUserFragment cannot call matches_text; reverse claim has to name a concrete type.

Adding an injection means a new file, a trait impl, and a hook into core/src/context/mod.rs. Thirty-nine modules in that directory. The friction itself is governance: a throwaway format! cannot get through, because the assembler takes Box<dyn ContextualUserFragment>.Source: codex-rs/core/src/context/mod.rs lines 3–41; codex-rs/core/src/session/mod.rs lines 3677–3707

Runtime fact Workspace, instructions, abort fragment type role · markers · body render Concatenated Message Afterwards only an InputText remains Ask a concrete type via type_markers; matches_text checks the ends; no instance needed to claim it back
Teaching diagram: Render walks the instance, claim walks the type; both calls read the same marker pair.
Why it lasts

Render and claim share one definition. Rewrite it in another language and the minimum is still one interface, one registry, the same marker pair. Type the assembler parameter as a fragment and the business side loses the call site for casually glued XML. The claim function only matches from the registry — no second text.startsWith.

Idea 2 · Spell out marked vs unmarked
What problem it solves

Mark every injection and a one-shot notice gets reclaimed after compact and fed again. Mark none and compacted history cannot tell user words from a runtime note. A fork will resubmit a stale <environment_context> as user input.

What the idea is

Window identity, environment, abort, image resize, managed developer instructions — later you need to claim them, diff them, decide whether to re-inject after compact, so they carry begin and end. One-shot notices — approved prefix, network-rule enrollment, a leftover-token reminder — have type_markers() return two empty strings. Default matches_text is always false.

The match only looks at the two ends of the whole text. Trim the start and compare the prefix, trim the end and compare the suffix, ASCII case-insensitive; both must hit. Whatever sits in the middle does not matter. An unmarked fragment gives up reversibility on purpose.Source: codex-rs/context-fragments/src/fragment.rs lines 89–103

The developer side has a second prefix table in event_mapping.rs, to patch some unmarked claims. Coverage is narrower than the user-side matcher list. The prefix table still keeps the old <token_budget> tag; the comment says it is there to claim wrappers persisted by older versions. Once a marker is written into a rollout, it is part of the restore contract. Changing a tag is changing the protocol.

UserInstructions has a spot that is easy to miss. Its begin marker is the Markdown heading # AGENTS.md instructions; the end marker is </INSTRUCTIONS>. The protocol also has the <user_instructions> pair — this struct does not use it. Write matches_text against the protocol constant and you will miss the text the repo actually renders.Source: codex-rs/core/src/context/user_instructions.rs lines 18–19; codex-rs/protocol/src/protocol.rs lines 112–113

Marked · claim later Env / window / abort begin + body + end matches_text can claim it Unmarked · say it once Approved prefix / net rules Body only Default claim given up Developer patches via prefix table
Teaching contrast: What must be reclaimed after compact becomes protocol; a one-shot notice gives up reversibility on purpose.
Why it lasts

What must be claimed later becomes protocol; a one-shot gives it up, and the type says so. What compact must reclaim forces begin and end. A one-shot notice may be unmarked, but the comment must say reversibility was given up.

Idea 3 · Assembly sorts by type fields
What problem it solves

Places casually push strings; order is a convention; standing alone lives in a comment. TokenBudgetContext gets squeezed into the same developer message as permission copy. The compact filter cannot process them per message. Mix them and rollback cannot split them. A comment in event_mapping.rs admits: build_initial_context may bundle a contextual fragment with durable developer text.Source: codex-rs/core/src/event_mapping.rs lines 69–71

What the idea is

First assembly happens in Session::build_initial_context_with_world_state. It bins fragments by role(), the start of markers(), and requires_separate_message(): mergeable developer spans, developer spans that must stand alone, user spans, and special spans that must sit at the top or bottom.

Window identity is a little special. When Feature::TokenBudget is on and the model has a context window, it is pushed into the separate bin before the world_state loop. Output order: one merged developer message, then each standalone developer message, then one contextual user message. The sort key is the type fields.Source: codex-rs/core/src/session/mod.rs lines 3630–3728

requires_separate_message() also puts “can it share a developer message” on the type. TokenBudgetContext, ImageResizeNotice, and ManagedDeveloperInstructions choose to stand alone. The cost is an extra message and an extra role switch. The gain: the compact filter can process them per message.

Types switched on Approved prefix Window identity Environment AGENTS.md Abort notice Sort role() markers().0 requires_separate Read fields, don’t scan the body developer · merge into one developer · each its own user · fold into one contextual
Teaching sort diagram: Output order is merged developer, standalone developer, contextual user.
Click any stretch of model input and you can walk it back to a fragment type.
Why it lasts

The assembly door only takes registered types. That is the usual shape for keeping unregistered injections outside. Types stop the unregistered; they do not stop a registered type that stuffs 40KB every turn. That layer is review rules — next lesson.

Side-by-side · Another answer to the same question

Where the gate sits: DeepSeek Harness reconciles at request time

DSH writes it at line 107 of the repo-root AGENTS.md. The Chinese principle folds to “model-visible means already logged.” Everything that reaches a model request must be rebuildable from the session log; a new model-visible input needs a new session event.

The runtime face is invariant.ts. It hangs a listener on llm/stream, derives the expected value from the session log via deriveMessages(), then JSON.stringify-compares it with the outgoing options.messages. Mismatch: fail. The gate sits at the crash point, so it can catch drift that only appears at runtime — messages rewritten after assembly. Turn off the invariants service and the gate is gone.

Source checked on both sides · 2026-08-22 · DSH · Model-visible ⟺ logged

Different sources of truth: the event log, or a closed set of types

DSH can live without a fragment trait because its source of truth is the event log; messages are a projection. Codex first folds anything that can appear in context into a closed set of types, then uses markers to claim them later.

The costs differ. Codex types stop you stuffing into render_full without a trait impl; they do not stop a dynamic string format!ed inside the impl. matches_text claims a text shape. Both sides pay for traceability: one pays an assertion on every request, the other pays type friction on every new injection.

Source checked on both sides · 2026-08-22
Classroom Exercise
01

Which stretch will you miss

The begin marker on UserInstructions is # AGENTS.md instructions; the protocol constant is <user_instructions>. If someone writes matches_text against the protocol constant, which actually-rendered text in the repo will they miss?

One step further: turn on the approved prefix in the demo above. After compact, can default matches_text still claim it? Which table on the developer side patches that cut, and will the compiler complain if you change the wrong constant?

Takeaway:Every stretch stuffed into model context first becomes a type that knows its own role, marker, and body. What must be claimed later carries begin and end; a one-shot notice gives up reversibility. Assembly sorts by type fields. An unregistered string cannot enter the envelope.