The Unified Exec system provides interactive PTY-based process execution that maintains long-lived shell sessions across multiple tool calls. Unlike the standard shell execution tools (Shell Execution Tools), Unified Exec creates persistent pseudo-terminal (PTY) processes that can be reused, enabling stateful interactive workflows where environment variables, working directories, and shell state persist across invocations.
This page documents the process management layer, including process lifecycle, ID allocation, output streaming, and background monitoring. For information about the tool handler interface, see Shell Execution Tools. For approval and sandboxing integration, see Tool Orchestration and Approval and Sandboxing Implementation.
Sources: codex-rs/core/src/unified_exec/mod.rs1-23 codex-rs/core/src/tools/handlers/unified_exec.rs1-27
Diagram: Unified Exec Component Architecture
The system has three main layers:
Sources: codex-rs/core/src/unified_exec/mod.rs24-49 codex-rs/core/src/unified_exec/process_manager.rs1-54 codex-rs/core/src/tools/handlers/unified_exec.rs26-27
Diagram: Process State Machine
Key Lifecycle Stages:
ToolOrchestrator handles approval and sandbox selectionopen_session_with_exec_envProcessStore for reuseExecCommandEndSources: codex-rs/core/src/unified_exec/process_manager.rs157-295 codex-rs/core/src/unified_exec/async_watcher.rs39-140
Process IDs uniquely identify unified exec sessions and enable the model to reference long-lived processes across multiple tool calls.
| Mode | Strategy | ID Range | Use Case |
|---|---|---|---|
| Production | Random | 1,000 - 99,999 | Non-deterministic IDs prevent collisions |
| Test/Deterministic | Sequential | 1000, 1001, 1002... | Predictable IDs for testing |
Diagram: Process ID Allocation Flow
The allocation logic (process_manager.rs106-133) ensures:
reserved_process_ids before being returnedFORCE_DETERMINISTIC_PROCESS_IDS is enabled, IDs increment sequentially starting from 1000Active processes are stored in ProcessStore:
Sources: codex-rs/core/src/unified_exec/mod.rs118-161 codex-rs/core/src/unified_exec/process_manager.rs68-84 codex-rs/core/src/unified_exec/process_manager.rs106-143
exec_command: Opening a SessionDiagram: exec_command Request Flow
Key Steps:
ToolOrchestrator applies security policiescodex_utils_ptyExecCommandBegin with process IDyield_time_ms for outputExecCommandEnd immediately, return process_id=NoneProcessStore, return process_id=SomeSources: codex-rs/core/src/unified_exec/process_manager.rs157-295
write_stdin: Interacting with SessionsDiagram: write_stdin Request Flow
Key Steps:
ProcessEntry from storeinput is non-empty, sends to PTY's stdinyield_time_ms for new outputexit_codelast_used, returns outputEmpty Polls: When input="", the system uses a longer minimum yield time (MIN_EMPTY_YIELD_TIME_MS = 5000ms) to avoid excessive polling (process_manager.rs329-336).
Sources: codex-rs/core/src/unified_exec/process_manager.rs297-390
Diagram: Output Streaming Architecture
Two Parallel Consumers:
Background Streamer (async_watcher.rs39-101):
broadcast::channelHeadTailBuffer (transcript)ExecCommandOutputDelta events (capped at MAX_EXEC_OUTPUT_DELTAS_PER_CALL)TRAILING_OUTPUT_GRACE (100ms) for final outputSynchronous Collector (process_manager.rs615-671):
OutputBuffer until deadline or process exitsexec_command (initial output) and write_stdin (interactive output)To prevent unbounded memory growth, output is stored in a HeadTailBuffer that retains:
UNIFIED_EXEC_OUTPUT_MAX_BYTES = 1MBWhen the buffer exceeds capacity, middle chunks are dropped, preserving both the beginning and end of the output.
Sources: codex-rs/core/src/unified_exec/async_watcher.rs39-172 codex-rs/core/src/unified_exec/process_manager.rs615-671 codex-rs/core/src/unified_exec/head_tail_buffer.rs
Diagram: Event Emission Timeline
Event Types:
ExecCommandBegin (async_watcher.rs178-209):
process_id if session will be long-livedExecCommandOutputDelta (async_watcher.rs162-170):
MAX_EXEC_OUTPUT_DELTAS_PER_CALL to avoid overwhelming clientsUNIFIED_EXEC_OUTPUT_DELTA_MAX_BYTES = 8KB per eventExecCommandEnd (async_watcher.rs178-209):
exec_command returnsexit_code, aggregated_output, and full transcriptTerminalInteraction (tools/handlers/unified_exec.rs222-229):
write_stdin to record user inputstdin content sent to the processSources: codex-rs/core/src/unified_exec/async_watcher.rs26-252 codex-rs/core/src/tools/events.rs89-109
Unified Exec integrates with the centralized ToolOrchestrator (Tool Orchestration and Approval) for approval and sandboxing:
Diagram: Orchestrator Integration Flow
Key Integration Points:
Approval Requirement (process_manager.rs573-584):
ExecApprovalRequest containing command, policies, and permissionsSandbox Selection (process_manager.rs561-613):
SandboxPolicy and SandboxPermissionsExecRequest with sandbox constraintsRuntime Execution (tools/runtimes/unified_exec.rs):
UnifiedExecRuntime implements the ToolRuntime traitUnifiedExecProcessManager.open_session_with_exec_envRetry on Denial (process_manager.rs561-613):
Sources: codex-rs/core/src/unified_exec/process_manager.rs561-613 codex-rs/core/src/tools/orchestrator.rs
Unified Exec supports deferred network approval for long-lived processes that may make network requests after the initial tool call completes:
Diagram: Deferred Network Approval Flow
Deferred vs Immediate:
| Mode | When Used | Lifecycle |
|---|---|---|
| Immediate | Short-lived commands | Approval tied to single tool call, unregistered when tool returns |
| Deferred | Long-lived sessions | Approval persists until process exits, stored in ProcessEntry |
When a process is stored with network_approval_id, the registration remains active so that network policy checks can continue to prompt the user for new hosts. When the process is later removed from the store (via write_stdin detecting exit or manual cleanup), the network approval is unregistered (process_manager.rs145-155).
Sources: codex-rs/core/src/unified_exec/process_manager.rs145-155 codex-rs/core/src/unified_exec/process_manager.rs260-274 codex-rs/core/src/tools/network_approval.rs1-67
To prevent resource exhaustion, the system enforces limits on active processes:
Diagram: Process Pruning Flow
Pruning Policies:
| Constant | Value | Purpose |
|---|---|---|
MAX_UNIFIED_EXEC_PROCESSES | 64 | Hard limit on concurrent processes |
WARNING_UNIFIED_EXEC_PROCESSES | 60 | Threshold for warning model |
When the store reaches capacity, the oldest process (by last_used timestamp) is pruned (process_manager.rs680-705). The pruning logic:
last_used (ascending)ProcessStoreWARNING_UNIFIED_EXEC_PROCESSESWarning Message (process_manager.rs504-512):
"The maximum number of unified exec processes you can keep open is 60 and you currently have N processes open. Reuse older processes or close them to prevent automatic pruning of old processes"
Sources: codex-rs/core/src/unified_exec/process_manager.rs469-525 codex-rs/core/src/unified_exec/mod.rs61-65
For long-lived processes, a background task monitors for exit and emits the final ExecCommandEnd event:
Diagram: Exit Watcher Lifecycle
The exit watcher (async_watcher.rs106-140) ensures that:
ExecCommandEnd with the complete transcript from HeadTailBufferGrace Period: The output streamer waits TRAILING_OUTPUT_GRACE = 100ms after exit is detected to capture any final output from the PTY before notifying the exit watcher (async_watcher.rs26-27 async_watcher.rs62-74).
Sources: codex-rs/core/src/unified_exec/async_watcher.rs106-140 codex-rs/core/src/unified_exec/async_watcher.rs178-209
| Constant | Value | Purpose |
|---|---|---|
MIN_YIELD_TIME_MS | 250ms | Minimum wait time for output |
MIN_EMPTY_YIELD_TIME_MS | 5,000ms | Minimum for empty write_stdin polls |
MAX_YIELD_TIME_MS | 30,000ms | Maximum wait time cap |
DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS | 300,000ms | Configurable max for background polls |
TRAILING_OUTPUT_GRACE | 100ms | Wait for final output after exit |
| Constant | Value | Purpose |
|---|---|---|
UNIFIED_EXEC_OUTPUT_MAX_BYTES | 1 MiB | Total retained output per process |
UNIFIED_EXEC_OUTPUT_MAX_TOKENS | ~256K tokens | Token-based limit (bytes / 4) |
UNIFIED_EXEC_OUTPUT_DELTA_MAX_BYTES | 8 KiB | Max size per delta event |
DEFAULT_MAX_OUTPUT_TOKENS | 10,000 | Default token limit for tool response |
Unified Exec sets specific environment variables to ensure consistent, non-interactive behavior (process_manager.rs55-66):
These variables are merged with the session's environment policy (process_manager.rs567-570).
Sources: codex-rs/core/src/unified_exec/mod.rs53-65 codex-rs/core/src/unified_exec/process_manager.rs55-91
The UnifiedExecResponse returned to the model includes structured metadata:
Format Function (tools/handlers/unified_exec.rs274-301) converts this to a text response:
Chunk ID: abc123
Wall time: 2.1234 seconds
Process exited with code 0
Original token count: 15000
Output:
<stdout content>
Or for long-lived sessions:
Chunk ID: def456
Wall time: 0.5000 seconds
Process running with session ID 1000
Output:
<stdout content>
The model can then reference the session_id (process_id) in subsequent write_stdin calls.
Sources: codex-rs/core/src/unified_exec/mod.rs105-116 codex-rs/core/src/tools/handlers/unified_exec.rs274-301
For integration testing, the system can be configured to use sequential process IDs:
This enables:
The flag is checked via cfg!(test) or the FORCE_DETERMINISTIC_PROCESS_IDS atomic flag (process_manager.rs68-84).
The test suite (core/tests/suite/unified_exec.rs) provides:
parse_unified_exec_output(): Parses structured tool responses (unified_exec.rs62-131)collect_tool_outputs(): Extracts outputs from request bodies (unified_exec.rs133-157)Sources: codex-rs/core/src/unified_exec/mod.rs46-48 codex-rs/core/src/unified_exec/process_manager.rs68-84 codex-rs/core/tests/suite/unified_exec.rs1-157
Refresh this wiki