## Why
We should be running as many integrations tests as possible against the
split cross-OS configuration.
## What
- migrate eligible thread starts and builders to auto env
- keep explicit custom/local-executor cases local with rationale
comments
- keep auto-env coverage where possible and add narrow `TODO(anp)` skips
for fixtures that are not target-native yet
## Why
`codex-rs/.github/workflows/cargo-audit.yml` is nested below the
repository root, so GitHub Actions never discovers or runs it. RustSec
advisory enforcement already runs through the root `cargo-deny`
workflow.
## What
Remove the inert nested Cargo audit workflow.
## Validation
- Ran `git diff --check`.
- Verified the workflow is absent from GitHub registered workflows and
that root blocking CI invokes `cargo-deny`.
## Why
Windows Cargo and Bazel jobs spend significant time in filesystem-heavy
build and cache directories. Route those directories through one CI
build root so Windows can use its Dev Drive and Unix can use a stable
cache root.
## What
- Have `setup-ci` define `CI_BUILD_ROOT`, `CARGO_TARGET_DIR`, Bazel
cache/output paths, and temp paths.
- Require Windows to find or provision a verified Dev Drive instead of
falling back to `C:`.
- Pass the shared Bazel output base to `setup-bazel` so its explicit
`output_base` does not defeat Dev Drive routing.
- Point nextest, release, and V8 source-build paths at the shared
environment contract.
## Benchmark results
One-off cold-cache WPR/ETW traces show the explicit Bazel output-base
routing removes the dominant `C:` traffic:
| sample | `C:\_bazel` | summed `C:` traffic | traced test step |
|---|---:|---:|---:|
| shard 1 before | 62.2 GiB | 85.2 GiB | 16m22s |
| shard 1 updated | 0 | 16.5 GiB | 12m05s |
| shard 3 before | 67.2 GiB | 84.6 GiB | 16m48s |
| shard 3 updated | 0 | 13.5 GiB | 11m08s |
For a cold x64 V8 source build, the retained build-tail sample showed
`D:\cargo-target` at ~1.29 GiB while measured `C:` roots totaled ~0.45
GiB (`C:\Users` ~0.33 GiB, `C:\Program Files` ~0.06 GiB, `C:\Windows`
~0.03 GiB). The full cold build took 2h20m36s.
The Bazel timing improvement is directional because both refreshed
shards failed tests. The V8 trace is a bounded build-tail sample, not
the full build. All final samples had zero lost ETW events; VHDX traffic
was excluded from the optimization ranking.
Runs: [baseline
Bazel](https://github.com/openai/codex/actions/runs/28911908527),
[updated
Bazel](https://github.com/openai/codex/actions/runs/28917133701), [V8
build tail](https://github.com/openai/codex/actions/runs/28933626678).
## Manual validation
- Ran `just fmt`.
- Ran `just test-github-scripts` (35 tests).
- Parsed GitHub Actions YAML with `yq`.
- Ran `git diff --check`.
## Stack
- [#31332](https://github.com/openai/codex/pull/31332) — parameterize
Cargo target paths
- [#31356](https://github.com/openai/codex/pull/31356) — Windows 2025
runner bump
- [#31357](https://github.com/openai/codex/pull/31357) — Dev Drive I/O
routing
## Why
Responses WebSockets are the normal lower-latency transport for
WebSocket-capable providers. They must not bypass an OS-selected proxy
when `features.respect_system_proxy` is enabled, but disabling
WebSockets whenever the feature is enabled would impose a substantial
performance penalty.
Merged PR #31622 introduced the reusable proxy-aware WebSocket
transport. This PR makes the Responses API its first consumer so the
existing fast path uses the same effective proxy and trust policy as
HTTP.
## What changed
- Register `codex-websocket-client` as a workspace dependency and use it
from `codex-api`.
- Feed the shared crate’s route-independent `WebSocketConnection` into
the existing Responses message pump.
- Require a configured `HttpClientFactory` for normal Responses
WebSocket connections and the CLI doctor probe, so neither path can open
a connection without consulting the effective proxy policy.
- Pass the session factory from `core` and the effective configuration
factory from `doctor`.
- Add an end-to-end Responses test that enables `RespectSystemProxy`,
asserts the resolved policy, completes a turn over WebSocket, and
verifies the connection and request counts.
- Keep the existing Responses protocol handling, ping/pong pump, and
session-scoped HTTP fallback unchanged.
The DNS, proxy, TLS, custom-CA, and Happy Eyeballs implementation and
its transport tests live in merged PR #31622. This PR deliberately
contains only the Responses integration and does not duplicate that
transport code.
## Review guide
1. `codex-rs/codex-api/src/endpoint/responses_websocket.rs` constructs
the shared connector and adapts its uniform stream to the existing pump.
2. `codex-rs/core/src/client.rs` supplies the session-scoped factory for
production Responses connections.
3. `codex-rs/cli/src/doctor.rs` supplies the effective configuration
factory to the handshake probe.
4. `codex-rs/core/tests/suite/client_websockets.rs` covers the
enabled-feature path end to end.
## Test plan
- `cargo check --tests -p codex-api -p codex-core -p codex-cli`
- `just test -p codex-api`
- `just test -p codex-core
responses_websocket_streams_with_system_proxy_feature`
- `cargo shear`
- `just bazel-lock-check`
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/31441).
* #31637
* #31431
* #31363
* #31362
* #31361
* __->__ #31441
## Description
This PR removes the last path in core that emits `ExecCommandBegin` /
`ExecCommandEnd` directly.
Every command execution now starts and completes through canonical
`ItemStarted` / `ItemCompleted(TurnItem::CommandExecution)`. The
existing `HasLegacyEvent` compatibility layer still fans out Begin/End
afterward, so raw core event consumers and legacy rollout replay keep
seeing the same events.
`UnifiedExecInteraction` is dormant today. Live unified exec uses
`UnifiedExecStartup` for command lifecycle and `TerminalInteraction` for
`write_stdin` and polling, so this is code cleanup rather than a current
product behavior change. The main win is the code-level invariant where
all core flows emit `TurnItem` instead of legacy events.
## What changed
- Removed the `UnifiedExecInteraction` branches that emitted legacy
command events directly.
- Routed every command source through the existing canonical
`CommandExecution` lifecycle and compatibility fanout.
## Why
Amazon Bedrock's GPT-5.6 variants currently appear as only `Sol`,
`Terra`, and `Luna`. Those labels omit the model family and version,
making them ambiguous in model lists and inconsistent with the naming of
other GPT models.
## What changed
- Rename the three Bedrock model display names to `GPT-5.6 Sol`,
`GPT-5.6 Terra`, and `GPT-5.6 Luna`.
- Strengthen the Bedrock model-manager test to verify both model IDs and
their propagated display names.
Model IDs, ordering, priorities, reasoning support, and default
selection are unchanged.
## Description
This PR moves hook prompts onto the canonical `TurnItem` lifecycle in
core.
Stop hooks now record their `ResponseItem` through the existing
lifecycle path, which emits `ItemStarted` and `ItemCompleted`.
App-server consumes those events directly instead of deriving a hook
prompt from `RawResponseItem`.
## Why
Hook prompts were the only `ThreadItem` app-server synthesized from
`RawResponseItem`. This brings them in line with other core-owned turn
items while preserving legacy rollout replay.
## What changed
- Route stop-hook prompts through
`record_response_item_and_emit_turn_item`.
- Materialize canonical hook prompts in `ThreadHistoryBuilder`.
- Remove `RawResponseItem` to `ThreadItem` synthesis while preserving
legacy rollout replay.
- Add focused coverage for lifecycle emission and canonical and legacy
history materialization.
Follow-up to #30226.
## Why
#30226 makes Apps World State inspect the MCP tool list, while
tool-router construction reads the same list again later in the sampling
request. `list_all_tools()` walks the MCP clients and may reconnect or
wait for tools, so doing that work twice adds latency and lets context
and tool construction observe different MCP states for one request.
## What
- Add a lazy MCP tool snapshot to `StepContext`.
- Reuse that snapshot for Apps World State and tool-router construction.
- Let each new `StepContext` refresh naturally for the next sampling
request, without manager-level caching or invalidation.
## Testing
- `just test -p codex-core apps_instructions`
- `just test -p codex-core
apps_guidance_appears_after_background_recovery_within_a_turn`
## Why
Codex Desktop installs its managed primary runtime under
`%USERPROFILE%\.cache\codex-runtimes`. Elevated Windows sandbox commands
run as dedicated sandbox users. The synchronous runtime ACL refresh
repairs read/execute access for the Desktop runtime directories under
`%LOCALAPPDATA%\OpenAI\Codex`, but did not include the managed primary
runtime cache.
As a result, the Desktop app could discover a bundled runtime while a
sandboxed command received `ACCESS_DENIED` when reading or executing it.
## What changed
- Include `%USERPROFILE%\.cache\codex-runtimes` in the managed runtime
paths considered by the Windows sandbox ACL refresh.
- Reuse the existing inherited read/execute ACL repair; no write
permission is added.
- Add Windows-target regression coverage for the runtime path list and
the primary-runtime-only case.
## Impact
Bundled Python, Node, and native tools remain usable from elevated
Windows sandbox sessions without broadening write access or granting
access to the rest of the user profile.
## Validation
- `just fmt`
- `just test -p codex-windows-sandbox` (10/10 host-side tests passed)
- Windows-target path tests included for CI
## Why
The route-aware WebSocket connection setup in #31441 is transport
infrastructure rather than Responses API protocol logic. Landing it
first in a dedicated crate keeps `codex-api` focused on request and
response behavior and makes the transport reusable by future WebSocket
clients.
WebSockets must also apply the same effective outbound proxy and
custom-CA policy as HTTP without disabling the lower-latency WebSocket
path. Requiring an `HttpClientFactory` when constructing the connector
makes proxy-policy resolution part of the API instead of an optional
call-site convention.
This PR is an independent prerequisite based directly on `main`. After
it merges, #31441 can rebase onto it and replace its in-crate connector
with this API.
## What changed
- Add a new `codex-websocket-client` workspace crate with a
`WebSocketConnector` constructed from the effective `HttpClientFactory`.
- Resolve every destination through that factory before connecting, then
support direct connections, transport-default routing, HTTP proxies, and
TLS-encrypted HTTPS proxies.
- Preserve custom-CA trust for proxy and target TLS handshakes and
preserve Happy Eyeballs fallback for explicit direct and proxy routes.
- Expose an established `WebSocketConnection` as a uniform `Stream` and
`Sink`, hiding route-specific transport types from protocol clients.
- Add focused integration-style coverage for the public connector and
message stream, real WSS over direct and CONNECT routes, implicit and
explicit HTTPS proxy ports, and stalled-address-family fallback.
## Review guide
1. `codex-rs/websocket-client/src/lib.rs` defines the small public API
and the factory-required policy invariant.
2. `codex-rs/websocket-client/src/dialer.rs` contains DNS, TCP, proxy
tunneling, TLS, and WebSocket handshake setup.
3. `codex-rs/websocket-client/src/dialer_tests.rs` verifies the public
stream, direct and proxied WSS paths, HTTPS port preservation, and Happy
Eyeballs timing.
4. There is intentionally no consumer migration here; #31441 will become
the first consumer after this prerequisite merges.
## Test plan
- `cargo check -p codex-websocket-client --tests`
- `just test -p codex-websocket-client`
- `cargo shear`
- `just bazel-lock-check`
## Why
Ultra reasoning may proactively use multiple agents. When
`features.multi_agent_v2.max_concurrent_threads_per_session` is
configured at 8 or higher, explicitly selecting Ultra can allow up to `N
- 1` subagents to work concurrently and increase usage quickly. Showing
the configured limits at selection time makes that tradeoff visible and
points users to the setting that controls it.
## What changed
- Show a warning history cell after the user explicitly selects Ultra
reasoning with a concurrent-thread limit of at least 8.
- Include the configured concurrent-thread count and maximum subagent
count in the warning.
- Apply the warning consistently across model and reasoning pickers,
Plan-mode scope selection, and reasoning shortcuts.
- Keep the trigger limited to the selected reasoning effort and
configured thread limit, independent of how multi-agent v2 is activated.
- Add focused threshold coverage and an `insta` snapshot for the
rendered warning.
## User impact
Users with high multi-agent concurrency receive a concrete warning
immediately after selecting Ultra. Other reasoning efforts, limits below
8, and startup behavior are unchanged.
https://github.com/user-attachments/assets/5795bcb9-432e-42bc-bde3-ee363e9aeb69
## Test plan
- `just test -p codex-tui ultra_reasoning_selection`
- Built the full debug CLI with `cargo build -p codex-cli`.
- Manually verified the debug TUI with a 10-thread limit: switching from
Max to Ultra displayed the warning with 10 concurrent threads and up to
9 subagents.
## Summary
- sanitize Claude-derived fallback titles without changing explicit
custom or generated source titles
- recognize known leading control wrappers, including `ide_opened_file`,
`ide_selection`, `local-command-stdout`, and `local-command-stderr`
- skip separate control-only user records and use the next meaningful
user message as the fallback title
- use `Imported session` only when every user record is control-only
- preserve raw imported messages, previews, and transcript provenance
unchanged
## Why
Sessions without an explicit source title fall back to user-message
content. Claude metadata can arrive as separate leading user records, so
stripping only the first record still exposed raw control markup or
produced an unhelpful fallback.
## Impact
Imported chats now receive readable fallback titles such as `Fix auth
flow`, while the original wrapper-bearing messages remain visible in the
imported transcript.
## Validation
- `just test -p codex-external-agent-sessions` — 37 tests passed
- `just test -p codex-app-server
external_agent_config_import_creates_session_rollouts` — focused
integration test passed
- `just fix -p codex-external-agent-sessions`
- `just fix -p codex-app-server`
- `just bazel-lock-check`
- `just fmt`
## Why
An untrusted exec-server can stop reading requests, never answer them,
or send guessed responses before queued requests are written. Without
client-side admission, the orchestrator can retain unbounded RPC call
futures and request payloads.
We need a hard bound without adding a blanket timeout, because
individual operations already own their timeout and cleanup semantics.
## What changed
- hold one of 64 shared admission permits for the full lifetime of each
regular RPC call, so an early response cannot free capacity while the
call remains queued
- allow `process/terminate` and `fs/close` to use one additional cleanup
permit
- close the transport and fail pending calls if the cleanup permit is
also stuck, allowing teardown or recovery to release remote resources
- leave the wire format and existing timeout behavior unchanged
## Tests
- `rpc_client_call_has_no_implicit_deadline` verifies that ordinary
calls remain untimed
- `rpc_client_bounds_in_flight_calls_and_preserves_cleanup` covers the
regular-call cap, guessed early responses, cleanup admission, and the
cleanup circuit breaker
## Summary
- on macOS, resolve the curated plugin sync Git executable without
executing it
- treat Apple’s `/usr/bin/git` shim as unavailable when `xcode-select
-p` reports that developer tools are absent
- skip directly to the existing GitHub HTTP fallback in that case
- preserve the original `git` command lookup on Windows and Linux,
including CCA
## Root cause
Curated plugin startup sync invokes `git ls-remote` before its HTTP
fallback. On a clean Mac, `git` resolves to Apple’s `/usr/bin/git` shim,
and executing the shim opens the Xcode Command Line Tools installer
before the process can fail and reach HTTP.
On macOS, this change resolves Git through `PATH` without executing it.
If the selected binary is Apple’s shim and developer tools are
unavailable, startup sync marks the Git transport unavailable and enters
the existing HTTP fallback immediately.
The new availability detection is macOS-only by construction. Windows
and Linux still execute the literal `git` command as before. If Git is
missing on Windows, the existing spawn-error path falls back to HTTP;
Linux/CCA receives no new lookup or startup behavior.
## Eager Git audit
I also audited production Git process spawns in `codex-rs`.
- This curated catalog sync is the only default projectless app-server
startup path found.
- Configured Git marketplace auto-upgrade runs Git at plugin startup,
but only after a user has explicitly configured a Git marketplace.
- Experimental Memories has background Git metadata/baseline paths when
the feature is enabled.
- The separate cloud-tasks UI probes Git during environment
autodetection.
- Normal thread/turn Git metadata is gated by filesystem discovery of an
existing `.git` entry.
- Marketplace add/install, patch apply, doctor, and TUI `/diff` paths
are explicitly user-invoked.
## Validation
- `just fmt`
- `just bazel-lock-update` — succeeded with no lockfile delta
- `just test -p codex-core-plugins` — 313 passed
- `just fix -p codex-core-plugins` — completed; emitted one pre-existing
unrelated `large_enum_variant` warning in `manifest.rs`
- `git diff --check`
## Description
Use UUIDv7 for item IDs generated locally in `codex-protocol`.
This covers user messages, agent messages, hook prompts, and context
compaction items. These IDs are generated once and carried through item
lifecycle, so this only changes the UUID version. It keeps generated
item IDs consistent with thread and turn IDs.
## Why
A pasted user message can contain a raw CSI sequence that corrupts
terminal scrollback, making the latest response or input appear missing.
The same sequence may already be present in persisted history when an
affected conversation is resumed.
## What changed
- Remove CSI sequences and non-whitespace control characters from
explicit paste input before it enters the composer.
- Apply the same small transform before rich and raw `UserHistoryCell`
rendering, so existing conversations render safely without mutating
stored message content.
## Summary
- Promote code_mode_host to stable and enable it by default.
- Preserve features.code_mode_host = false as an opt-out to the
in-process runtime.
- Run core code-mode tests through the standalone host.
- Keep explicit coverage for missing-host failures.
## Why
The legacy unelevated Windows sandbox allowed tools to create and update
files in workspace-write roots, but it could not delete files that
already existed there. This breaks operations such as `apply_patch` file
deletion and replacement in the workspace, `TEMP`, and `TMP`.
The delete grant must also preserve deny-write carveouts. Granting
`FILE_DELETE_CHILD` on a writable parent would let the sandbox remove
protected children such as `.git` or an explicit read-only subpath even
when those children have direct deny ACEs.
This addresses the delete-failure variant reported in #30009 and #30712.
It does not claim to fix their separate split-root setup,
elevated-helper, or proxy-related failure modes.
## What Changed
- Give writable-root capability ACEs inheritable `DELETE` rights without
granting parent-level `FILE_DELETE_CHILD`, so descendants can be removed
while protected children remain protected.
- Replace stale write ACEs that still contain `FILE_DELETE_CHILD`, and
make elevated setup detect and refresh that unsafe legacy state.
- Keep read-only capability handling unchanged.
- Add Windows regressions covering pre-existing files in the workspace,
`TEMP`, and `TMP`, plus protected `.git` and outside-root controls.
The core ACL behavior is in
[`acl.rs`](767540eec3/codex-rs/windows-sandbox-rs/src/acl.rs (L303-L438)),
stale-ACE detection is in
[`setup_main/win.rs`](767540eec3/codex-rs/windows-sandbox-rs/src/bin/setup_main/win.rs (L163-L179)),
and the end-to-end regression is in
[`unified_exec/tests.rs`](767540eec3/codex-rs/windows-sandbox-rs/src/unified_exec/tests.rs (L458-L568)).
## How to Test
On Windows:
1. Start Codex with the legacy unelevated Windows sandbox and a
workspace-write permission profile.
2. Seed pre-existing files in the workspace, `TEMP`, and `TMP`; also
create a sibling file outside the writable roots and a protected `.git`
directory.
3. Delete the three files inside writable roots through a sandboxed
command or `apply_patch`.
4. Confirm the writable-root files are deleted, while the outside-root
file and protected `.git` directory remain intact.
Targeted tests:
- `just test -p codex-windows-sandbox`
- Windows-only
`legacy_workspace_write_delete_is_limited_to_writable_roots`
- Windows-only `write_root_refresh_replaces_stale_delete_child_grant`
The final SHA passed all 31 required checks, including the Windows Bazel
test matrix, in [run
28886245161](https://github.com/openai/codex/actions/runs/28886245161).
## Summary
MCP operation timeout errors currently print the full debug precision of
the remaining timeout budget. That makes a configured 30-second timeout
show up as something like `29.999999875s`.
This PR rounds the displayed duration to a whole unit, so the error is
clean and stable:
```text
timed out awaiting tools/list after 30s
```
The timeout behavior itself is unchanged; this only affects the
human-facing error text. A regression test covers the reported
`tools/list` case.
## Why
Finish the TestAppServer builder migration after every caller has moved
off the compatibility constructors.
## What
- remove the obsolete public TestAppServer constructors
- leave TestAppServer::builder() as the only fixture construction API
## Validation
- cargo check -p codex-app-server --tests
## Cleanup stack
1. [#31425 test: add TestAppServer
builder](https://github.com/openai/codex/pull/31425)
2. [#31451 test: migrate TestAppServer callers to
builder](https://github.com/openai/codex/pull/31451)
3. this PR
## Description
This PR moves review-mode markers onto canonical `TurnItem` lifecycle:
- `TurnItem::EnteredReviewMode`
- `TurnItem::ExitedReviewMode`
Core now emits `ItemStarted` / `ItemCompleted` for both. The completed
items map back into the existing `EnteredReviewMode` /
`ExitedReviewMode` events, so raw core event consumers and legacy
rollout persistence keep seeing the old events.
This is the compatibility layer needed before paginated rollouts persist
review markers as `ItemCompleted(TurnItem)`.
## Why
Review markers were one of the remaining app-server thread items created
directly from legacy events. Giving them canonical items lets paginated
history persist stable turn/item IDs without changing legacy rollouts.
## What changed
- Added canonical review-mode `TurnItem`s and switched review flow to
emit their lifecycle.
- Added completed-item → legacy review event mappings with stable
turn/item IDs.
- Switched app-server live notifications to the generic canonical item
path and kept legacy replay compatible with old payloads.
- Updated `ThreadHistoryBuilder` to replay canonical review items even
though review turns still do not emit `TurnStarted`.
## Description
This PR migrates standalone web search onto the extension-owned
turn-item path introduced in #31283.
Standalone web search now emits `ExtensionItem::WebSearch` through
generic `TurnItem::Extension`, while app-server still exposes the
existing typed `ThreadItem::WebSearch` JSON shape. Hosted Responses API
web search stays on core-owned `TurnItem::WebSearch`.
## What changed
- Added `web_search::WebSearchItem` and `WebSearchAction` to
`codex-extension-items` under the stable `web.search` kind.
- Collapsed `ExtensionTurnItem` to generic `{ item, legacy_events }` now
that no typed extension special cases remain.
- Kept the existing `WebSearchBegin` / `WebSearchEnd` compatibility
events and canonical-first ordering.
- Updated app-server projection/history and generated TypeScript; the
app-server JSON schema is unchanged.
## Why
The Codex JavaScript shim currently distinguishes npm and Bun installs,
but a global pnpm install falls back to npm. That causes the native CLI,
`codex doctor`, and update flows to report or run npm commands even
though pnpm owns the installation. pnpm also allows its global package
and bin directories to differ, so `PNPM_HOME` does not reliably identify
the package owner.
## What changed
- detect pnpm-managed installs by finding pnpm's
`node_modules/.modules.yaml` metadata from the launched JavaScript
entrypoint
- pass a mutually exclusive `CODEX_MANAGED_BY_PNPM` marker into the
native CLI
- represent pnpm in install context, doctor output, version checks, and
TUI update actions without changing the existing public
`InstallContext::from_exe` signature
- recommend `pnpm add -g @openai/codex` for pnpm-managed installs and
snapshot the rendered TUI update notice
Validated with the 9-test install-context suite, the focused pnpm TUI
snapshot test, and Node's syntax check for the launcher.
Closes https://github.com/openai/codex/issues/10294.
## Why
`subagent_usage_draws_from_the_shared_budget` intermittently fails even
when the shared-budget behavior is correct. `ResponseMock` records a
request before the custom Wiremock predicate is checked, so the
follow-up mock can also contain unrelated requests. In a [recent Windows
ARM64 run](https://github.com/openai/codex/actions/runs/28916285431),
`single_request()` saw all seven requests from the scenario.
## What changed
Select the request containing the follow-up user prompt before making
assertions. The test still requires exactly one matching follow-up
request and still checks that the root sees 50 tokens remaining after
the child uses its share.
This is test-only. Shared-budget behavior and the common response-mock
helper are unchanged.
## Summary
- classify plugin store and remote bundle I/O failures by privacy-safe
operation context
- include the normalized `sub_error_type` in structured plugin-install
failure warnings
- leave analytics events and schemas unchanged
## Why
Top-level error types such as `store_io` collapse multiple filesystem
operations, making logs harder to diagnose. The subtype identifies the
failed operation without logging paths or new exception details.
## Impact
Plugin-install warning logs gain an optional normalized subtype.
Analytics payloads, app-server APIs, and stored data are unchanged.
## Validation
- `just fmt`
- `just test -p codex-core-plugins -p codex-app-server` (1,235 passed;
14 failed and 6 timed out because local `sandbox-exec` was denied,
`test_stdio_server` was unavailable, or user-level skill fixtures were
loaded)
## Why
The remote compaction parity test compares legacy and v2 sessions
created with separate temporary homes. Those sessions can discover
different model-visible skill catalogs, so the request comparison can
fail even when the compaction and service-tier behavior matches.
This is the most frequent retry-saved full-CI failure in the recent
JUnit history.
## What changed
Normalize only the contents of `<skills_instructions>` before comparing
the captured requests. The opening and closing tags remain in the
comparison, so the test still catches a missing or misplaced skills
block.
The service-tier, compacted input, follow-up request, and
replacement-history assertions are unchanged. A focused normalizer test
covers the new behavior.
## Scope
This is test-only. It does not change runtime compaction or skill
behavior. Exact skill-catalog rendering remains covered by the dedicated
skills tests.
## Why
The encrypted MAv2 spawn test often reads the parent follow-up request
before the child has sent its first request. The response mock records
candidate requests before applying its specific matcher, so the test can
see an empty `agent_message` list even though delivery happens a moment
later.
## What changed
Wait for the recorded child request that contains `agent_message`, using
the same short bounded polling pattern already used in this test module.
The exact encrypted payload and communication-log assertions stay
unchanged.
This is test-only: it does not add product delays, loosen the assertion,
or change multi-agent behavior. No follow-up is expected.
## Summary
Selection rows reserve their first two columns for the selection cursor
and render into the menu surface's left gutter. The empty-state message
reused that shifted row area even though it has no cursor prefix, so `no
matches` appeared two columns to the left of the search field.
This renders empty lists at the inset list origin while preserving the
cursor gutter for non-empty rows.
Why
Filtering hidden directories after a walk is too late: their descendants
consume traversal limits, and canonical directory deduplication can let
a hidden path claim a target before a visible symlink reaches it.
What this changes
- Add an optional pruneHiddenDirectories walk option. It defaults to
false and is omitted from the wire when disabled.
- Return hidden directory entries, but do not traverse them or add their
canonical identities to the visited set.
- Cover the visible-symlink-to-hidden-directory case through both local
and remote filesystem implementations.
This is the small filesystem prerequisite for #31566. Skill-specific
behavior remains in that PR.
## Description
This PR adds a `codex-extension-items` crate for extension-owned
`TurnItem` schemas, and updates standalone image generation to start
using it via `TurnItem::Extension`.
This gives us a way to prevent Core from having to be aware of all
extension items. App-server still exposes the existing public
`ThreadItem::ImageGeneration` shape, now by wrapping the same shared
`image_generation::ImageGenerationItem` type.
The new `codex-extension-items` crate is necessary because the image gen
extension item is used by:
- `codex-image-generation-extension`, which produces it.
- `codex-tools / core`, which carry it generically.
- `codex-protocol`, which serializes it into lifecycle events and
rollouts.
- `app-server protocol`, which wraps it in public
`ThreadItem::ImageGeneration`
```
extension implementation
↓
codex-extension-items
↓
protocol / tools / app-server
```
We keep the hosted Responses API image generation as
`TurnItem::ImageGeneration` because core still owns its persistence and
legacy fanout.
### Before
Standalone image generation is implemented as an extension, but its item
representation previously lived in the core protocol. This sets the
precedent that core is aware of all extension items, which would be good
to avoid.
```
image-gen extension
→ constructs codex_protocol::ImageGenerationItem
→ emits ExtensionTurnItem::ImageGeneration
→ core matches ImageGeneration specially
→ protocol stores TurnItem::ImageGeneration
```
### After
```
image-gen extension
→ constructs extension-owned ImageGenerationItem
→ emits generic ExtensionItem
→ core transports/persists it generically
→ app-server wraps ImageGenerationItem as ThreadItem::ImageGeneration
```
Future extension items can have typed app-server APIs without adding a
new `TurnItem` variant, `ExtensionTurnItem` variant, or core emitter
match arm.
## What changed
- Added `codex-extension-items` with the closed `ExtensionItem` enum and
shared `image_generation::ImageGenerationItem` schema.
- Added generic `TurnItem::Extension(ExtensionItem)` and
`ExtensionTurnItem::Extension { item, legacy_events }` paths.
- Updated standalone image generation to emit a typed extension item and
provide its existing legacy `ImageGenerationBegin` /
`ImageGenerationEnd` events explicitly.
- Kept canonical lifecycle ordering: core emits `ItemStarted` /
`ItemCompleted` before extension-provided legacy events.
## Follow-up
Standalone web search still uses its typed special-case path. Migrating
it later would let `ExtensionTurnItem` collapse into a single
extension-item struct.
## Why
The V8 Windows source jobs need a runner image that can support the
later Dev Drive setup.
## What
Move the two V8 Windows source-build matrix entries from `windows-2022`
to `windows-2025`.
Namespace the canary source-build cache by runner image so it cannot
restore Windows 2022 native outputs.
## Manual validation
- Ran `just fmt`.
- Ran `just test-github-scripts` (33 tests).
- Parsed GitHub Actions YAML with `yq`.
- Ran `git diff --check`.
## Stack
- [#31332](https://github.com/openai/codex/pull/31332) — parameterize
Cargo target paths
- [#31356](https://github.com/openai/codex/pull/31356) — Windows 2025
runner bump
- [#31357](https://github.com/openai/codex/pull/31357) — Dev Drive I/O
routing
Adds a single trace span around configured hook command execution so
slow installed hooks are visible directly in traces.
The span records low-cardinality hook metadata and command outcome
without command text, hook input, stdout, or stderr.
## Summary
External auth had two paths: provider-command credentials were resolved
through `ExternalAuth`, while app-provided ChatGPT credentials were
installed separately and only used the provider for refresh.
`ExternalAuth` also declared an auth mode independently from the
`CodexAuth` value it returned, so the declaration and credentials could
disagree.
This change makes the provider-owned `CodexAuth` authoritative for
initial resolution, credential kind, and refresh.
- remove `ExternalAuth::auth_mode` and require providers to return their
current auth from `resolve`
- route external auth registration, resolution, and unauthorized refresh
through `AuthManager::set_external_auth`
- keep the last resolved credential with its provider so synchronous
consumers and unauthorized recovery observe the credential's actual mode
- make the app-server bridge own both initial and refreshed ChatGPT
credentials, and detach it on logout
- keep model listing free of auth-refresh side effects, including in
offline mode
Provider-command auth still follows its configured cache interval.
App-provided ChatGPT auth still asks the parent app once after a `401`
and retries the request once.
Stacked on #31355.
## Testing
- `just test -p codex-login`
- `just test -p codex-models-manager`
- focused `codex-app-server` tests for external login/logout,
unauthorized refresh, and workspace mismatch
## Why
Prepare CI jobs for a later build-output relocation without changing
where they write today.
## What
- Export `CARGO_TARGET_DIR` from `setup-ci` at the existing
`codex-rs/target` path.
- Route nextest, release, artifact, and signing paths through
`CARGO_TARGET_DIR`.
- Require V8 staging callers to pass an explicit target directory while
preserving the existing upstream path.
## Manual validation
- Ran `just test-github-scripts`.
- Parsed GitHub Actions YAML with `yq`.
## Stack
- [#31332](https://github.com/openai/codex/pull/31332) — parameterize
Cargo target paths
- [#31356](https://github.com/openai/codex/pull/31356) — Windows 2025
runner bump
- [#31357](https://github.com/openai/codex/pull/31357) — Dev Drive I/O
routing
## Summary
Opening `/review` and choosing a branch currently enumerates branches
through `git branch`, which performs broader branch presentation work.
In a large repo, it isn't hard for this to timeout completely.
This PR instead uses `git for-each-ref` scoped directly to `refs/heads`
so the picker reads only local branch refs. It preserves the existing
sorting and default-branch promotion while excluding remote-tracking
refs and detached-HEAD display rows.
## Summary
- Rename the hosted web-search wire field to the canonical
`indexed_web_access` spelling.
- Preserve existing indexed-search behavior.
## Rollout
Merge and release only after server support for `indexed_web_access` is
fully deployed.
## Testing
- `just fmt`
- `just test -p codex-tools
web_search_tool_spec_serializes_expected_wire_shape`
- Blocking CI passed, including indexed web-search integration coverage.
## Why
Loading skills from a remote executor can add a lot to thread start time
when there are many skills. Previous changes added some concurrency for
the file reads themselves, but we're still bottlenecked on the initial
root path discovery.
Using the benchmark from
[#31295](https://github.com/openai/codex/pull/31295), this change
reduces the measured mean of loading 66 skills about 71%. Behavioral
coverage lands separately in
[#31369](https://github.com/openai/codex/pull/31369).
## What
- resolve the scanned root's inherited namespace once
- resolve discovered nested plugin roots once, retaining
nearest-valid-ancestor behavior
- pass an explicit resolved Plain / Plugin namespace into skill parsing
instead of probing per skill
- preserve explicitly provided plugin namespaces as the highest-priority
source
- reuse the same resolver for environment skills, deleting its duplicate
root-probe and ancestor-selection path
## Validation
- just test -p codex-core-skills namespace: 12 passed on both the parent
and optimized branches
## Why
#31335 lets HTTP callers obtain proxy-aware clients from
`HttpClientFactory`, but a non-HTTP transport such as WebSockets also
needs two pieces of policy owned by `codex-http-client`: a concrete
route decision for its destination and the same custom-CA-aware rustls
trust configuration used by HTTPS.
Keeping these prerequisites in the shared abstraction means the
dependent Responses WebSocket change (#31441) cannot independently
reinterpret `features.respect_system_proxy`, PAC results, or enterprise
CA settings.
## What changed
- Add a redaction-safe `OutboundProxyRoute` with explicit
transport-default, direct, and concrete-proxy outcomes.
- Add `HttpClientFactory::resolve_proxy_route()` so transports can
resolve a destination through the already-selected outbound proxy
policy.
- Resolve `ws://` and `wss://` URLs through their HTTP equivalents so
system and PAC rules apply consistently.
- Add an always-returned rustls config builder that starts from native
roots and layers in any configured Codex custom CA bundle. The existing
optional builder remains available to callers that can delegate the
default configuration to their transport.
- Continue redacting proxy URLs from `Debug` output because they may
contain credentials.
## Review guide
1. `http-client/src/outbound_proxy.rs` defines the transport-neutral
route result and WebSocket URL normalization.
2. `http-client/src/custom_ca.rs` factors the native-root/custom-CA
construction so callers that perform TLS themselves can always obtain a
config.
3. `http-client/src/outbound_proxy_tests.rs` verifies WebSocket
normalization and legacy transport-default behavior.
## Test plan
- `just test -p codex-http-client outbound_proxy`
- `just test -p codex-http-client custom_ca`
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/31342).
* #31431
* #31363
* #31362
* #31361
* #31442
* #31441
* __->__ #31342
## Why
Test callers need one composable way to create app-server fixtures
instead of a growing family of overlapping constructor implementations.
## What
- add a feature-complete TestAppServer::builder()
- make the default builder own a temporary CODEX_HOME and select the
automatic test environment
- expose builder knobs for no automatic environment, explicit
CODEX_HOME, program, arguments, plugin startup tasks, environment
overrides, managed config, and JSON logging
- keep the existing public constructor surface, but route every
constructor through the builder so the new path is exercised immediately
- remove the redundant private constructor ladders; caller migration and
public constructor removal live in the optional cleanup stack
## Validation
- just test -p codex-app-server (940/941 before updating the expected
builder error wording)
- just test -p codex-app-server
auto_env_rejects_explicit_environment_config
- just fix -p codex-app-server
- just fmt
## Follow-up stacks
Cleanup, optional for the benchmark work:
1. [#31451 test: migrate TestAppServer callers to
builder](https://github.com/openai/codex/pull/31451)
2. [#31452 test: remove TestAppServer
constructors](https://github.com/openai/codex/pull/31452)
Benchmark infrastructure:
1. [#31427 test: add delayed exec-server
transport](https://github.com/openai/codex/pull/31427)
2. [#31295 bench: add cold skill load
macrobenchmark](https://github.com/openai/codex/pull/31295)
3. [#31428 bench: add e2e benchmark
entrypoints](https://github.com/openai/codex/pull/31428)
4. [#31429 ci: smoke Bazel e2e
benchmarks](https://github.com/openai/codex/pull/31429)
## Summary
Autocomplete completion previously inserted a new space even when a
separator was already present, which could leave redundant whitespace
around neighboring text. Popup dismissal also tracked only the query
string, so dismissing one token could suppress a different occurrence
with the same text.
This gives completions one horizontal-separator policy across files,
images, skills, and mentions. Existing separators are reused when
possible, ordinary suffix text remains separated, and line breaks stay
intact. Dismissal now identifies the complete whitespace-delimited token
occurrence, so offset-only edits preserve dismissal while later
identical tokens remain independent. Newly completed values beginning
with `$` or `@` can also remain closed when the next PR's affinity rule
recognizes the token to the left of the cursor.
## Examples
The examples below use `|` to represent the cursor.
### Reuse existing separators
Starting with an existing two-space gap:
```text
@ma| next
```
After accepting `src/main.rs` and typing `foo`, completion previously
added a third separator and left both original spaces before `next`:
```text
src/main.rs foo| next
```
After this PR, completion reuses the first existing separator as the
insertion point and preserves the second between the new text and
`next`:
```text
src/main.rs foo| next
```
### Keep a completed sigil-prefixed value closed
Starting before a line break:
```text
@ma|
next
```
and accepting a path whose result is itself prefixed with `@` produces:
```text
@scope/main.rs |
next
```
Without this PR's completion dismissal, the affinity rule in #30463
rediscovers the completed token to the left of the cursor and reopens
its popup:
```text
@scope/main.rs |
^^^^^^^^^^^^^^^ popup reopens
next
```
With this PR, the inserted occurrence is dismissed and the popup remains
closed:
```text
@scope/main.rs |
^^^^^^^^^^^^^^^ popup remains closed
next
```
### Do not dismiss an identical later occurrence
Given two identical tokens:
```text
@scope/main.rs| @scope/main.rs
```
after dismissing the first popup with Escape and moving to the second
token, query-only dismissal previously suppressed the second popup too:
```text
@scope/main.rs @scope/main.rs|
^^^^^^^^^^^^^ popup remains closed
```
After this PR, dismissal also matches the token's ordinal among complete
tokens, so the second occurrence opens normally:
```text
@scope/main.rs @scope/main.rs|
^^^^^^^^^^^^^ popup opens
```
### Keep dismissal across offset-only edits
After dismissing `@ma`, moving to its start, and pasting an email-like
token:
```text
email@ma.com @ma|
```
the `@ma` bytes embedded in `email@ma.com` do not count as another
autocomplete token. The original `@ma` keeps its dismissal even though
its byte range moved.
## Stack
This is PR 2 of 3, stacked on #31190. It relies on the explicit
replacement ranges introduced there and provides the completion
lifecycle used by the targeting fix in #30463.
## Summary
- recognize embedded HTTP(S) URL prefixes case-insensitively in Windows
dangerous-command detection
- add regression coverage for uppercase and mixed-case schemes inside
`Start-Process` invocations
## Why
PowerShell and URL parsing treat schemes case-insensitively, but the
pre-parser only searched for lowercase `http://` and `https://`. When a
URL appeared in the same shlex token as surrounding PowerShell syntax,
such as `Start-Process('HTTPS://example.com');`, the prefix was not
stripped and the command was incorrectly classified as not dangerous.
Validated with the scoped `codex-shell-command` suite (138 tests) and a
direct classifier reproduction that failed before the change and passed
afterward.
## Why
Pre-sampling compaction intentionally uses the previous turn's model
when the compaction compatibility hash changes or when switching to a
model with a smaller context window. This keeps compaction aligned with
the settings that produced the history, but it can block the next turn
when a resumed ChatGPT thread still references a model slug that has
since been retired. The Codex backend rejects that compaction request
before the user's currently selected model gets a chance to sample.
This PR lets those threads recover without changing previous-model
compaction behavior for API-key authentication or custom providers. It
is stacked on #31316, which is a behavior-preserving extraction of the
individual remote compaction attempts; this PR contains the fallback
behavior.
## What changed
- For automatic previous-model compaction, capture the selected model's
request context when using ChatGPT authentication with the OpenAI
provider and the selected model differs from the previous model.
- If the previous-model attempt returns an `InvalidRequest`, retry
compaction once with the selected model for both `/responses/compact`
and Responses Compaction V2.
- Complete history processing, lifecycle events, and token accounting
with the context of the model that successfully compacted the thread.
- If the fallback also fails, return the original previous-model error
so the retry does not change the user-visible failure.
- Record fallback attempts with reason, implementation, and outcome
telemetry.
- Leave API-key authentication, custom providers, same-model turns, and
non-`InvalidRequest` failures on their existing paths.
## Testing
- `just test -p codex-core -E 'test(pre_sampling_compact) |
test(model_unavailable_error)'` (10 tests)
- Added integration coverage for a resumed thread whose model was
renamed, a model downshift using Responses Compaction V2, and API-key
authentication with a custom provider.
This PR depends on [#31296](https://github.com/openai/codex/pull/31296)
for the canonical-to-legacy event mappings.
## Description
This PR makes the v1 and v2 wait paths emit canonical
`TurnItem::CollabAgentToolCall` lifecycle instead of
`CollabWaitingBegin` / `CollabWaitingEnd` directly.
Both paths already used the same legacy waiting events before this PR.
The v1 item carries receiver metadata and final agent statuses for its
target agents; v2 waits for mailbox activity rather than specific
agents, so it keeps those fields empty, matching the existing v2 legacy
payload.
App-server v2 consumes the canonical item directly and ignores the
mapped legacy wait events.
## Why
Wait is separate from the other collab tools because it is multi-target
and has distinct timeout/status behavior. Keeping it last also lets this
PR remove the old helper that only existed to shape legacy wait status
entries in core.
## What changed
- Emit canonical collab wait items from both v1 and v2 wait handlers.
- Preserve receiver metadata and agent status snapshots on completed
wait items.
- Remove the old core helper for building legacy wait status entries.
## Follow-up
The next stack PR, [#30188](https://github.com/openai/codex/pull/30188),
writes canonical `TurnItem` values to paginated rollout files.
This PR depends on [#31296](https://github.com/openai/codex/pull/31296)
for the canonical-to-legacy event mappings.
## Description
This PR makes the non-wait v1 collaboration tools—spawn, send input,
resume, and close—emit canonical `TurnItem::CollabAgentToolCall`
lifecycle instead of their legacy begin/end events directly.
App-server v2 consumes the canonical collab items directly, ignores the
mapped legacy events, and applies close-agent thread-watch cleanup from
the completed item.
## Why
These four tools share the same single-target lifecycle shape. Wait
stays separate because it carries multi-target status snapshots and has
its own status-shaping cleanup.
## What changed
- Add shared helpers for emitting canonical collab tool-call lifecycle.
- Migrate spawn, send input, resume, and close handlers.
- Move close-agent watcher cleanup onto canonical completed collab
items.
## Why
The backend config-bundle contract now exposes managed configuration in
`managed_layers`, split into `baseline` and `system_overlay`. The Rust
transport models need to match that contract before any runtime behavior
changes.
## What changed
- add the generated `DeliveredManagedLayers` model
- model `baseline` and `system_overlay` as required arrays
- expose optional/null `managed_layers` on delivered config and
requirements documents
- retain `enterprise_managed` for transport compatibility
This PR changes transport types only; cloud-config runtime behavior is
unchanged.
## Stack
1 of 4. Next: #31286.
## Validation
- `just test -p codex-backend-openapi-models -p codex-backend-client`
- `just test -p codex-cloud-config`
- revalidated against the current generated backend schema
This PR depends on [#31296](https://github.com/openai/codex/pull/31296)
for the canonical-to-legacy event mappings.
## Description
This PR makes the MultiAgentV2 spawn, message/follow-up, and interrupt
paths emit completed canonical `TurnItem::SubAgentActivity` items
instead of `SubAgentActivityEvent` directly.
App-server v2 now applies interrupted-agent thread-watch cleanup from
the canonical completed item and ignores the mapped legacy activity
event.
## Why
Sub-agent activity is separate from the v1 collab tool begin/end
lifecycle. Keeping it separate makes the v2 watcher side effect
reviewable without mixing in the larger collab tool-call migration.
## What changed
- Emit canonical sub-agent activity items from v2 spawn,
message/follow-up, and interrupt paths.
- Move missing-thread watcher cleanup onto canonical completed activity
items.
- Update focused app-server coverage to exercise canonical interrupted
activity.
## Why
This PR is a behavior-preserving refactor only. It does not add a
fallback, change which model is used for compaction, or otherwise change
compaction behavior. The behavioral change is implemented in the stacked
follow-up, #30319.
Pre-sampling compaction deliberately uses the previous turn's context
when the compaction compatibility hash changes or when switching to a
model with a smaller context window. That preserves the model settings
that produced the history being compacted, but the previous context is
not always usable. For example, a resumed thread can still reference a
model slug that has since been retired, causing compaction to fail
before the currently selected model can sample.
#30319 addresses that failure mode by retrying compaction with the
current turn's selected model when the backend rejects the
previous-model attempt. This PR performs only that preparatory refactor.
## What changed
- Extracted one legacy `/responses/compact` request attempt into
`compact_remote_request.rs`.
- Extracted one Responses-based remote compaction request attempt into
`compact_remote_v2_attempt.rs`.
- Kept hooks, lifecycle events, analytics, window advancement, history
processing and installation, and error behavior unchanged in the
existing orchestration paths.
- Preserved standalone Responses-based compaction's owned client-session
lifetime through lifecycle completion.
## Testing
- `just test -p codex-core -E 'test(remote_compact)'` (22 tests)
## What
Adds an optional hosted login-success redirect path for app-server login
requests.
- Keeps the existing localhost success page as the default.
- Lets app-server callers opt into a hosted success page with an
optional protocol field.
- Persists credentials before redirecting to the hosted success page.
- Keeps org setup and existing CLI/device-code login flows on the local
success page.
- Accepts an optional typed `appBrand` value and forwards it to the
hosted page as `app_brand` so web can select the correct asset.
- Generates the app-server protocol schema updates for the new optional
fields.
## Why
This supports the hosted Codex login success page rollout without
changing existing login behavior by default. The Codex Apps frontend can
gate the opt-in with Statsig after the hosted web page.
## Rollout safety
- Old callers omit the new field and continue using localhost.
- New callers talking to old app-server builds remain safe because the
Codex Apps side treats the field as optional and defaults the flag off.
- Missing brand values default to Codex.
- The hosted redirect always uses the app-login source so the hosted
page can reopen Codex; the existing streamlined-login visual flag
remains separate.
## Validation
- `just fmt`
- `just fix -p codex-login -p codex-app-server-protocol -p
codex-app-server -p codex-app-server-test-client -p codex-tui`
- `just test -p codex-login`
- `just test -p codex-app-server-protocol`
- `just write-app-server-schema`
- `git diff HEAD --check`
The focused login and protocol run passed all 380 tests.
I also started the broader `just test -p codex-app-server`; it compiled
successfully, then many tests failed on this machine because spawned
test servers tried to use the ambient `/Users/rafaelj/.codex/sqlite`
state DB, which is read-only in this sandbox. I stopped that run after
confirming the failures shared that environment issue.