Special Topic · Inside OpenAI Codex

exec and wait: How an Unfinished Program Ends

Codex lets the model write JavaScript to orchestrate tools: a V8 isolate with capabilities subtracted, and a long-task protocol that yields the cell when time runs out, then continues with wait

THE QUESTION THIS PAGE ANSWERS

ANSWER FIRST

What is the key idea behind “exec and wait: How an Unfinished Program Ends”?

Codex lets the model write JavaScript to orchestrate tools: a V8 isolate with capabilities subtracted, and a long-task protocol that yields the cell when time runs out, then continues with wait

DECISION RULE

Follow the handoffs, not the demo. A system becomes dependable at the boundaries between model, tools, state, permissions, and people. Read each handoff as a place where you can observe, test, and recover.

TRY NEXT

Name the input, owner, approval, and recovery action for one automated step.

WATCH FOR

A successful run that cannot explain what happened or be safely repeated.

Course goalAfter reading, you can explain two things: why the model’s runtime is subtracted down to no Node, no filesystem, no network, not even console; and why Codex records an over-budget program as “still running,” not as a failure.
Try it first · Budget hits — kill, or yield?
The same program: six subtasks, about two seconds each, twelve seconds total
Budget
Left treats it as a wall-clock ceiling; right treats it as a yield interval. Raise it to 20 seconds and the difference disappears.
Stop at the deadline0/6 kept0delivered
Waiting to start.
Yield at the deadline0/6 done0round trips
Waiting to start.
Logic trail · which source span each animation step maps to
  1. The model can put a pragma on the first line, declaring how much yield time this run getsdescription.rs L22
  2. The exec entry turns that millisecond count into “observe once when the clock hits”service.rs L77
  3. A budget over ten seconds gets one extra second of grace, then a server-side capservice.rs L198
  4. When the timer hits, the buffered output is handed over as one packet, and the buffer is clearedcell_actor/mod.rs L242
  5. The yield is translated into a sentence the model can read, with a cell idcode_mode/mod.rs L283
  6. The model comes back with the id to continue — and can also change the budget, cap length, or stop itwait_handler.rs L24
  7. The script finishes, returns a result, and closes the cellruntime.rs L24
Hit Play to see how the same program ends under two timeout policies.
Where the work goesOn the stop-at-deadline side, finished subtask results vanish with the process. The model gets one timeout message and no usable intermediates.
The cost of continuingOn the yield side, the model spends extra round trips and gets all six subtasks done — and sees new progress on every yield.
When the budget is enoughSet the budget to 20 seconds and both sides finish in one run. The two policies only differ when the budget is short.
Teaching sketch: subtask counts and timings are course settings, to show the structural difference between two timeout policies. Line numbers on the logic trail match openai/codex commit 4f39251a01.
Idea 1 · Give the model a runtime, then carve it down
What problem it solves

Read five files and summarize: ordinary tool calls take five full round trips. Each trip pushes the whole file into context, and the next turn the model re-reads the swollen history. What’s expensive is the rhythm of the trips, not the tools.

So just let the model write a program. The trouble: nobody reviewed a line of it — the model writes and hands it over live. Give it a runtime that can read files and hit the network, and you’ve handed over the host’s whole capability set.

What the idea is

Codex gives the model two tools, exec and wait. exec takes a stretch of JavaScript and evaluates it as an async module in a fresh V8 isolate. Every tool hangs off the global tools object, names normalized to legal JS identifiers, so you write await tools.exec_command(...). Loop if you want, branch if you want; intermediates stay in variables; only what you hand over comes back to the model.

The point is how thin this runtime was carved. The tool spec tells the model, straight, what it does not have:

codex-rs/code-mode-protocol/src/description.rslines 20–25
- Runs raw JavaScript -- no Node, no file system, no network access, no console.
- Accepts raw JavaScript source text, not JSON, quoted strings, or markdown code fences.
- You may optionally start the tool input with a first-line pragma like `// @exec: {"yield_time_ms": 10000, "max_output_tokens": 1000}`.
- `yield_time_ms` asks `exec` to yield early if the script is still running. Defaults to 10000 ms.
- `max_output_tokens` sets the token budget for direct `exec` results. Defaults to 10000 tokens.
- When the JS code is fully evaluated, the isolate's lifetime ends and unawaited promises are silently discarded.
Source snapshot note: based on the local openai/codex repo; verified against codex-rs/code-mode-protocol/src/description.rs, commit 4f39251a01, verified 2026-08-22. Code blocks keep the original source. This text itself is the tool spec sent to the model.

No Node, no filesystem, no network, not even console. Those weren’t forgotten — they were withheld. Any side effect has one road: tools. That road still has the same approvals and sandbox: the dialogs still pop, the blocks still block.

A side benefit: the surface you have to audit shrinks. If the isolate could read files, this layer would need its own file-permission set. Now it can do nothing, so permission stays one layer down — you don’t write the code twice.

Call one by one Model Tool Each trip needs a sample; intermediates go whole into context Write a program Model One sample V8 isolate No Node, filesystem, network, or console; side effects go through tools One stretch of JavaScript Only what you hand over
Teaching diagram: same job — many trips above, one below.
Why it lasts

Trips are expensive, batching is cheap — an old ledger. Databases have bulk writes; RPC frameworks accumulate batches. One model sample costs far more than one network hop, so collapsing N trips into one pays even bigger.

The subtraction half is more general. Give untrusted code the smallest room you can, so even if it wants harm it has no interface — that’s the shape of secure design, and it doesn’t depend on V8. Swap the language or the sandbox and the question is the same: which few capabilities does this code actually need, and can the rest be withheld entirely?

Idea 2 · Not finished isn’t failure — it’s still running
What problem it solves

The program needs three minutes. What do you set the timeout to?

Set it to three minutes and the user stares at silence, with nowhere to shout stop. Set it to ten seconds and long work never finishes — worse, the first nine seconds vanish with it, and the model gets one timeout and has to start over. Both directions are wrong. The bug is treating “not done yet” as failure.

What the idea is

Codex makes a running script a thing with an identity: a cell. When yield_time_ms hits, the cell doesn’t die — it hands over the buffered output as one packet, clears the buffer, and keeps running. exec then returns a sentence: the script is still running, here’s the id.

With the id, the model has three choices: call wait to buy more time; pass terminate: true to stop it; or go do something else first. wait returns only output since the last yield, because the buffer was cleared on handoff — the same text doesn’t occupy context twice.

Stop at the deadline Script running; output sits in the buffer Wall clock hits; process killed Buffered output vanishes with it Budget hits Yield at the deadline Script running; output sits in the buffer Hand over the packet; clear the buffer Script keeps running wait continues Only the new stretch Budget hits
Teaching sequence: the same instant — one side kills the process, the other hands over work and keeps running.

A small detail shows what the designers were thinking. When yield time is over ten seconds, Codex adds one extra second of grace before it actually observes.Source:codex-rs/code-mode-runtime/src/service.rs lines 198–210A script that finishes right on the boundary doesn’t burn an extra round trip over a few milliseconds.

One more asymmetry is worth a note. In the spec sent to the model, wait has four parameters: cell id, yield time, return-length cap, and whether to terminate. The protocol request struct only carries the first two. The last two stop at the handler: terminate takes another path, and the length cap is applied after the result arrives. Reading the source, those two layers are easy to mash into one.

Budget hits: don’t kill it — hand in the work first.
Why it lasts

Making “not finished” a first-class state is the shape of long-task APIs. HTTP has 202 plus polling; job queues have a job id plus poll; a big-file export also hands you a number first. The common move: don’t force the caller to choose between “wait forever” and “treat as failure” — give them a handle they can ask again.

On an Agent, that handle is worth one more layer. After seeing intermediates the model can change its mind — the first four steps look wrong, so it terminates, instead of sitting through the next eight minutes. Control returns to the side that can think.

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

Timeout: DeepSeek Harness chooses to kill

DSH’s run_code keeps two ledgers. One books busy time by polling the worker’s event-loop utilization — hot loops can’t hide, and idle waits on slow tools aren’t billed unfairly. The other books wall clock and kills the worker when it hits. Defaults: 60,000 ms and 600,000 ms.

The cost is clear: one run_code must finish inside the budget; timeout is failure; there is no first-class “same program keeps running.” What you get is a simple implementation — the host doesn’t keep a pile of live cells. Codex flips it: the model must learn a wait protocol, and a cell occupies session resources until it finishes, is stopped, or the session ends.

Both sides verified against source · 2026-08-22 · DSH · Code Mode

State: a disposable world, or a drawer you keep

DSH’s design notes are blunt: the world the program lives in dies with the worker — no pooling, no cross-run state. Anything for the next run goes into the tool result or a workspace file. The upside: every run is a clean new world, easy to replay when something breaks.

Codex gives store and load: multiple execs in one session can share data; across sessions they can’t see each other. Orchestration is easier; cleanup falls back on you. The drawer has no per-entry size cap, only refuses values that won’t serialize to JSON, and has no TTL — it waits for the whole session to end, then goes with the runtime.

Both sides verified against source · 2026-08-22
Classroom Exercise
01

How to price a yield budget so you don’t lose

A program needs forty seconds; the default yield budget is ten. Walk it: how many waits does the model send, does each return the full output or only the new stretch, and why raising the default to thirty seconds is not always cheaper.

Follow-up: if at second 35 the program puts a large object into store, then the model decides to terminate, when is that object cleared, and who owns its size?

Takeaway:Let the model write a program and collapse N trips into one. Subtract the runtime: no Node, filesystem, network, or console; side effects walk the already-approved tool road. Budget hits — hand in the work, then continue. Make “not done yet” first-class so results don’t vanish and control returns to the model.

The handoffs inside “Try it first · Budget hits — kill, or yield”

“Read five files and summarize: ordinary tool calls take five full round trips.” shows that an Agent is not defined by the model alone. Each handoff between model, context, tools, state, permissions, and people affects both progress and recovery.

Write the state before adding capability

Starting from “So just let the model write a program.”, split the workflow into starting state, next action, tool result, state update, and stop condition. Debugging then means finding the first lost piece of information or authority instead of saying vaguely that the model “got worse”.

  • The model can put a pragma on the first line, declaring how much yield time this run gets description.rs L22
  • The exec entry turns that millisecond count into “observe once when the clock hits” service.rs L77
  • A budget over ten seconds gets one extra second of grace, then a server-side cap service.rs L198

A happy path is not reliability

Use “Follow-up: if at second 35 the program puts a large object into store , then the model decides to terminate , when is that object cleared, and who owns its size” to replay one successful and one failed run. Record the context, tool result, and owner at each turn; the workflow is maintainable when a second person can follow it without the original builder.

From “Try it first · Budget hits — kill, or yield” to “Idea 1 · Give the model a runtime, then carve it down”

“Try it first · Budget hits — kill, or yield” grounds the problem in “The same program: six subtasks, about two seconds each, twelve seconds total Play Step Reset Budget 4 s 8 s 20 s Left treats it as a wall-clock ceiling; right treats it as a yield interval. Raise it to 20 secon…”. “Idea 1 · Give the model a runtime, then carve it down” then moves it toward “Read five files and summarize: ordinary tool calls take five full round trips. Each trip pushes the whole file into context, and the next turn the model re-reads the swollen history. What’s expensive is the rhy…”. Together, they show that the lesson is not just a conclusion to remember, but a claim with conditions.

Carry the judgment into the next situation

When analyzing an Agent, trace state, action, tool result, and next step in order. Each handoff should explain where information came from, who confirmed it, and where failure stops.

  • “Try it first · Budget hits — kill, or yield”: The same program: six subtasks, about two seconds each, twelve seconds total Play Step Reset Budget 4 s 8 s 20 s Left treats it as a wall-clock ceiling; right treats it as a yield interval. Raise it to 20 secon…
  • “Idea 1 · Give the model a runtime, then carve it down”: Read five files and summarize: ordinary tool calls take five full round trips. Each trip pushes the whole file into context, and the next turn the model re-reads the swollen history. What’s expensive is the rhy…
  • “The closing point”: The yield is translated into a sentence the model can read, with a cell id code_mode/mod.rs L283

The final “The closing point” brings the discussion to “The yield is translated into a sentence the model can read, with a cell id code_mode/mod.rs L283”. The useful thing to carry forward is knowing which judgments must be revisited when input, scale, or risk changes.

Mark as learned Your reading progress updates automatically
← PreviousNext →

Keep reading

The next useful article in the thread.

ARTICLE DISCUSSION

Leave one useful thought here.

Keep the idea that clicked, the question that stayed open, or a small note for the next learner.

Discussing exec and wait: How an Unfinished Program Ends Inside OpenAI Codex
3discussionsArticle discussion · synced with the Circle
View in the learning circle
AM
Asha MorganContent editor
INSIGHTField note

I turned one judgment from this article into a small experiment I could run today. Knowing what to observe next is more useful than simply remembering the conclusion.

ARTICLE DISCUSSION7 helpful
LH
Lin HarperIndie developer
INSIGHTInsight

After reading this, I first looked for the conditions behind the idea instead of copying the method into a project. That order made the later trade-offs much clearer.

ARTICLE DISCUSSION5 helpful
KM
Kiki MooreProduct operations
QUESTIONQuestion

When this judgment reaches real work, which constraint should be added first? I am curious which step matters most between reading and the first practical attempt.

ARTICLE DISCUSSION4 helpful