[Codex Thread
019edd6d-6f14-74e2-853c-345d1803d4a6](https://codex-thread-link.openai.chatgpt-team.site/thread/019edd6d-6f14-74e2-853c-345d1803d4a6)
## Stack
Review and merge in order. Every layer is independently correct and
documents its safe stopping point.
1. [openai/codex#30292](https://github.com/openai/codex/pull/30292) —
aggregate File/Secrets store locking
2. [openai/codex#30293](https://github.com/openai/codex/pull/30293) —
resolve and lifecycle-pin the exact OAuth store
3. [openai/codex#30416](https://github.com/openai/codex/pull/30416) —
serialized authoritative refresh transaction
4. [openai/codex#30294](https://github.com/openai/codex/pull/30294) —
Codex-owned transport refresh and one-shot 401 recovery
5. [openai/codex#30295](https://github.com/openai/codex/pull/30295) —
login/logout transaction serialization
6. [openai/codex#30296](https://github.com/openai/codex/pull/30296) —
diagnostic-only Auto store drift reporting
**This PR is layer 2.**
## Why
`Auto` is keyring-first with a File fallback, but re-evaluating that
policy during transport reconstruction or persistence can make one MCP
client read from one store and later write to another. With rotating
refresh tokens, the second store may contain an older token. This layer
makes the source selected at client startup explicit and keeps that
authority stable for the client lifecycle.
## What this PR does
- Keeps `resolve_oauth_tokens_from_store_policy` as the single
configured-policy entry point and returns both credentials and the
concrete File or Keyring source that supplied them.
- Puts exact `load`, `save`, and `delete` operations on
`ResolvedOAuthCredentialStore`, making “resolve configured policy” and
“use the selected authority” distinct at call sites.
- Pins the first concrete source in `pinned_credential_store` in the
transport recipe, so initialization retries and session reconstruction
cannot re-evaluate `Auto` and adopt another store.
- Gives `OAuthPersistor` the resolved store and keeps subsequent
persistence and removal on that authority.
- Uses a typed keyring-load error to distinguish aggregate-store
coordination failures from ordinary backend failures; a coordination
failure is surfaced instead of triggering File fallback.
- Keeps login-time `Auto` behavior unchanged: prefer Keyring, fall back
to File when unavailable, and clean up legacy File state after a
successful keyring save.
- Adds structured server/backend context when fallback cleanup fails.
## Explicit decisions and non-goals
- The selection is lifecycle-local and in memory. This PR does not add a
durable backend selector, migration, reconciliation registry, or global
source of truth outside `CODEX_HOME`.
- `Auto` may choose File at the start of a later process if keyring
availability changes. Once this client resolves, a selected-store
failure is returned instead of hot-switching.
- Different `CODEX_HOME` instances remain independent even when they can
access the same Direct keyring credential.
- Cross-process refresh serialization is intentionally not part of this
layer.
## Safe stopping point
This PR can merge alone. A single MCP client no longer hot-switches
credential stores across transport rebuilds or persistence. Two
processes can still refresh the same selected credential concurrently
until layer 3.
## Review size
The net layer is 9 files, +668/−144. The production change remains
focused on store resolution and lifecycle pinning; the largest follow-up
is integration coverage that drives real session recovery.
## Validation
- `just test -p codex-rmcp-client` (99 passed; 5 expected skips)
- Real-client 404 recovery coverage with different Keyring and File
tokens; captured bearer headers prove the stale File token is never sent
- Mutation check: removing the lifecycle pin makes that integration
regression fail by observing the stale File token
## Why
Not every Codex distribution currently includes the
`codex-code-mode-host` companion binary. Enabling the process-host
feature should not make code mode unavailable on those surfaces while
packaging support is being completed.
## What changed
- Fall back to an in-process code-mode session only when spawning the
companion binary returns `io::ErrorKind::NotFound`.
- Keep permission, handshake, timeout, and other host failures visible
instead of silently falling back.
- Store the provider's owned-process/in-process choice as one
enum-backed state so later sessions reuse the fallback decision.
- Preserve the underlying spawn `io::Error` while retaining the host
path in the displayed error.
- Update provider, `CodeModeService`, and end-to-end coverage to verify
successful fallback execution.
## Test plan
- `just test -p codex-code-mode`
- `just test -p codex-core missing_process_host`
## Summary
Codex Apps file parameters are exposed to the model as local paths,
uploaded at execution time, and rewritten into provided-file payloads
before the MCP tool call.
The rewrite currently includes the documented optional fields
`mime_type` and `file_name` for every file parameter. Apps with strict
schemas can reject those fields when they are not declared.
## Changes
- Derive the supported optional file fields from each
`openai/fileParams` parameter's raw input schema before replacing it
with the model-visible local-path schema.
- Always include `download_url` and `file_id`.
- Include `mime_type` and `file_name` only when that specific file
parameter's schema accepts them, including schemas that allow additional
properties.
- Handle scalar and array file parameters, including items-only arrays,
composed schemas, and local JSON Schema references.
- Preserve the existing restriction that only the host-owned Codex Apps
MCP server can use this upload path.
This supports tools with different file contracts in the same app: one
parameter can accept the optional fields while another remains strict.
## Validation
- `just test -p codex-mcp`
- `just test -p codex-core mcp_openai_file`
- `just test -p codex-core codex_apps_file_params_`
- `just fix -p codex-mcp`
- `just fix -p codex-core`
- `just fmt`
- `git diff --check`
- Manually verified in the Codex Electron app that:
- a strict file schema receives only `download_url` and `file_id`
- a rich file schema also receives `mime_type` and `file_name`
Related: #31330
## Why
Codex Apps file parameters use a three-step upload flow: create a file
record, PUT bytes to a returned signed URL, and finalize the upload.
Each step still constructed a default `reqwest` client, so the flow
could bypass `features.respect_system_proxy` even after model API
requests honored it.
This stack entry makes the resolved client policy a required input to
the upload API and resolves each concrete destination independently.
## What changed
- Require `HttpClientFactory` in `upload_openai_file`.
- Build clients for the create, signed upload, and finalize URLs through
the shared API route policy.
- Pass the factory derived from the turn configuration at the Apps/MCP
call site.
- Return a destination-aware `ClientBuild` error when enabled route
selection cannot construct a client.
- Preserve the legacy logged fallback for the feature-off
`ReqwestDefault` policy.
## Review guide
1. `codex-api/src/files.rs` changes the upload API and centralizes
route-aware client construction.
2. The three request stages each supply their actual URL, including the
separately hosted signed blob URL.
3. `core/src/mcp_openai_file.rs` is the only production caller and
supplies the turn configuration factory.
## Validation
- `cargo check --tests -p codex-api -p codex-core`
- `just test -p codex-api files` (1 matching upload test passed; 135
tests skipped by filter)
- `just fix -p codex-api -p codex-core`
## Follow-up
Other direct HTTP clients remain separate migration slices.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/31363).
* #31637
* #31431
* __->__ #31363
## Summary
- Forward Codexs canonical `originator` header on ChatGPT-hosted Apps
and plugin-runtime MCP requests.
- Preserve the configured `X-OpenAI-Product-Sku` header.
- Cover originator-only and originator-plus-SKU configurations.
## Why
Sites project creation is logged downstream of Apps MCP. Production
validation found `CODEX_UNKNOWN_DEFAULT` project-created threads that
matched `codex_surface=desktop_app` and `originator=Codex Desktop` in
`fact_codex_cli`
([query](https://kepler.gateway.data-1.internal.api.openai.org/permalink/H_mVoVPqLQ0)).
The hosted Apps MCP configuration forwarded the product SKU but not
Codexs canonical originator, so codex-backend could not derive
`CODEX_DESKTOP_APP` for those tool calls.
## Validation
- `just fmt`
- `CARGO_HOME=/private/tmp/codex-cargo-home
CARGO_TARGET_DIR=/private/tmp/codex-target just test -p codex-mcp` (106
passed)
- `CARGO_HOME=/private/tmp/codex-cargo-home
CARGO_TARGET_DIR=/private/tmp/codex-target just fix -p codex-mcp`
## Why
`NetworkProxyConfig` only wrapped `NetworkProxySettings` in a single
`network` field. That extra level made runtime callers repeat `.network`
everywhere without representing a real boundary.
## What changed
- move the managed-network fields directly onto `NetworkProxyConfig`
- collapse the matching partial-config wrapper
- update runtime callers and tests to use the direct fields
- keep the user-facing permissions/profile TOML layout unchanged
The internal serialized shape now matches the runtime type itself. This
does not change managed-network behavior or the `config.toml` shape.
## Why
When MCP tools are deferred behind `tool_search`, Code mode keeps them
callable but omits their individual declarations from the initial `exec`
description. The shared MCP `CallToolResult` types were derived only
from directly rendered tools, so deferring every MCP tool also removed
the common response contract that models need to interpret MCP results.
This restores that contract without undoing the context savings from
deferred tool definitions. This is a follow-up to #29486.
## What changed
- Track deferred Code-mode tool definitions separately from directly
rendered definitions.
- Render the shared MCP type preamble when either direct or deferred MCP
tools are available.
- Keep deferred tool declarations out of the initial prompt.
- Add unit and integration coverage for deferred MCP tools.
## Testing
- `just test -p codex-code-mode-protocol`
- `just test -p codex-core
code_mode_only_guides_all_tools_search_and_calls_deferred_app_tools`
## Why
`handle_non_tool_response_item` logged the complete decoded
`ResponseItem` at DEBUG. Those values can contain assistant text,
reasoning content, and tool payloads that are already stored in the
durable rollout, making the SQLite copy both redundant and potentially
large or sensitive.
## What changed
- Replace the complete item dump with bounded `item_type` and `item_id`
fields.
- Keep an event-flow breadcrumb for debugging without duplicating item
content.
- Leave rollout persistence and tool-call telemetry unchanged.
The item-type match is exhaustive so new response item variants must
choose an explicit log label.
Related to #28224.
## Why
A thread can select skill roots that live in an executor environment.
`skills/list` needs a passive snapshot of the roots that are usable now:
it must not start an executor, wait for recovery, or reconnect a failed
environment.
The initial implementation checked the immutable first startup result.
After a successful connection later entered recovery or failed, that
result still looked successful. A read-only catalog request could then
wait for recovery or trigger a new connection while reading the
filesystem.
## What
- inspect readiness from the current exec-server connection state
- return roots only while their environment can serve a request
immediately
- omit environments that have not started, are connecting, or are
recovering
- return warnings for missing environments and terminal connection
failures
- add a fail-fast filesystem view that never starts, waits for, or
reconnects an environment
- expose the passive selected-root snapshot through `CodexThread`
## Behavior
- Local and currently connected environments are ready.
- Starting and recovering environments are omitted without a warning so
callers can retry later.
- Missing and terminally failed environments are omitted with a warning.
- A disconnect between readiness inspection and filesystem access fails
promptly instead of crossing into the normal recovery path.
- Normal model-turn and execution paths keep their existing reconnect
behavior.
## Design
The recovery policy is private to the exec-server client. Callers choose
the explicit fail-fast filesystem method; the existing client and
filesystem APIs remain reconnecting. This keeps the passive contract at
the transport boundary instead of plumbing timeout or retry flags
through the skills stack.
## Coverage
- a lazy stdio environment stays unstarted during passive inspection
- missing and terminally failed environments surface warnings
- a real websocket disconnect proves current readiness drops, a
previously acquired fail-fast filesystem handle returns promptly, and
readiness returns after recovery
## Scope
This PR only provides passive readiness and fail-fast filesystem
primitives. It does not add app-server API fields or notifications.
## Stack
- #31582 uses these primitives for experimental thread-scoped
`skills/list`.
- #30228 adds targeted invalidation notifications.
## Why
CCA thread startup loads repository skills through the primary remote
environment. This still used the older host skill loader, which
recursively issued directory and metadata requests over the exec-server
connection. On a high-latency connection, one skill scan could turn into
hundreds of round trips.
Executor-selected skill roots already avoid this pattern: they use one
bounded filesystem walk, reuse the returned inventory, and overlap
independent reads.
## What changed
- Extract the URI-native walk inventory into a shared private discovery
module.
- Use that discovery path for both environment-owned and host/repository
skill roots.
- Reuse the walk inventory to avoid per-skill metadata existence checks
when the inventory is complete.
- Canonicalize host skill identities and parse skill files with bounded
concurrency.
- Prune hidden host directories during traversal, preserving visible
aliases and walk limits.
- Cache namespace probes by ancestor and keep all remote namespace reads
under the same concurrency bound.
- Keep safe metadata probes for incomplete walks, file symlinks, and
case aliases.
## Dependency
#31570 is now merged. It provides the optional `pruneHiddenDirectories`
walk flag used here. That flag defaults to false and is omitted when
disabled.
No other exec-server API changes are introduced.
## Why
`ModelClient` already carries the `HttpClientFactory` resolved from
session configuration, but realtime call creation and memory
summarization still constructed the legacy default client directly.
Consequently, those first-party API requests could ignore
`features.respect_system_proxy` even when Responses traffic honored it.
These are the final direct default-client constructions in
`core/src/client.rs`, so they form one small migration unit on top of
#31361.
## What changed
- Generalize `build_responses_transport` to `build_api_transport`.
- Route realtime call creation through the helper using the selected
provider and `/realtime/calls` destination.
- Route `/memories/trace_summarize` through the same helper.
- Remove the now-unused direct `build_reqwest_client` import.
## Review guide
1. The helper rename at the bottom of `core/src/client.rs` is mechanical
and keeps existing Responses behavior unchanged.
2. The realtime call path computes the route from the final provider,
including `api_provider_override`.
3. The memories path supplies its existing endpoint to the same API
route class.
## Validation
- `cargo check --tests -p codex-core`
- `just fix -p codex-core`
## Follow-up
Direct HTTP clients outside `ModelClient` remain separate migration
slices.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/31362).
* #31637
* #31431
* #31363
* __->__ #31362
## Description
This PR makes new threads with `history_mode = "paginated"` persist
`ItemCompleted(item: <turn_item>)` in their rollout JSONL file.
Legacy threads keep persisting the existing legacy events. Because the
format is selected per thread, a rollout is either legacy or paginated;
we do not need to support mixed rollouts containing both
representations.
This PR depends on [#31473](https://github.com/openai/codex/pull/31473).
## Why
Paginated thread history needs stable turn/item IDs and completed item
snapshots so the later SQLite projector can materialize appended rollout
JSONL without rebuilding the whole thread.
Keeping the legacy persistence policy unchanged avoids changing
historical rollouts or the readers that still consume them.
## What changed
- Made rollout filtering history-mode aware. Paginated threads keep
completed canonical `ItemCompleted` events and drop their redundant
legacy projections; legacy threads keep the existing event set.
- Made forks inherit the source thread history mode, so copied legacy
history is never filtered as paginated.
- Made paginated threads assign IDs to locally-created response items
even when `Feature::ItemIds` is off, and reject streamed output items
that arrive without server IDs.
- Updated legacy turn replay, rollout list/search, and SQLite metadata
extraction to understand completed canonical user-message items.
[Codex Thread
019f2408-dc59-79f2-b245-4c11debd1a61](https://codex-thread-link.openai.chatgpt-team.site/thread/019f2408-dc59-79f2-b245-4c11debd1a61)
## Why
Long-lived Codex sessions can outlive the ChatGPT bearer token that was
present when the MCP runtime started.
The Responses path already recovers from token expiration by refreshing
or reloading the shared `AuthManager`. The reserved `codex_apps`
hosted-plugin client did not observe that update: `McpConnectionManager`
built its `/ps/mcp` HTTP auth once from a `CodexAuth` snapshot, and
`auth_provider_from_auth` copied that snapshot bearer into a static
`BearerAuthProvider`.
After the copied bearer expired, `/ps/mcp` kept sending it even though
Responses had a newer token in the same `AuthManager`. The failure
occurred before downstream connector execution, so unrelated apps such
as Gmail, Slack, and Google Calendar could all fail with the same
transport-level `401 token_expired`.
This replaces
[openai/codex#29474](https://github.com/openai/codex/pull/29474), which
was closed for inactivity without being merged. A new long-lived-session
report reproduced the same simultaneous `/ps/mcp` expiry pattern across
unrelated apps.
## What changed
- Add an `AuthManager`-backed request-header provider in
`codex-model-provider`. It keeps an `Arc<AuthManager>` and reads
`auth_cached()` for each outbound request, so the next `/ps/mcp` call
sees a token refreshed by the existing Responses/auth-recovery flow.
- Scope that provider to the startup account, ChatGPT user, and
workspace identity. Same-identity token reloads are followed; an account
switch emits no ambient auth until account-scoped MCP state is rebuilt.
- Have `McpConnectionManager` construct the dynamic provider only for
the reserved `codex_apps` registration used by the hosted-plugin
`/ps/mcp` path.
| MCP path | Auth behavior after this change |
| --- | --- |
| Reserved `codex_apps` hosted-plugin `/ps/mcp` | Read current
same-identity auth from the shared `AuthManager` per request |
| `codex_apps` with `CODEX_CONNECTORS_TOKEN` | Keep the environment
bearer-token override |
| User-configured/direct MCP registrations | Keep their existing
configured auth path |
## Non-goals
- No plugin-service changes.
- No downstream Slack, Gmail, Calendar, or other connector
OAuth/link-refresh changes.
- No auth UI changes.
- No behavior change for user-configured/direct MCP registrations.
- No new `/ps/mcp`-initiated token refresh; this makes `/ps/mcp` observe
refreshes already performed through the shared `AuthManager`.
## Tests
- `just test -p codex-model-provider`
- Covers same-identity token reloads and refuses a changed startup
identity.
- `just test -p codex-mcp`
- `just test -p codex-core mcp_auth_refresh`
- Creates the reserved hosted-plugin `codex_apps` `/ps/mcp` client
before the shared `AuthManager` changes, updates that same manager
through its public external-auth path, performs a real `tools/call`, and
asserts the request uses the current bearer.
## Why
A thread resumed without an explicit reviewer could pick up the reviewer
from the current config instead of preserving the reviewer already in
use by the thread. After an app restart, this meant a thread running
with auto review could silently switch back to user review, and the next
turn could continue under the wrong reviewer.
## What changed
Persist the effective reviewer with each turn and restore the latest
persisted value when the thread resumes. If the resume request
explicitly provides a reviewer, that value still takes precedence.
## Test plan
- Added a regression test that starts a thread with auto review, records
a turn, restarts with user review in config, resumes without an
override, and verifies that auto review is preserved.
- `just test -p codex-protocol`
- `just test -p codex-state`
- `just test -p codex-rollout`
- `just test -p codex-app-server
thread_resume_preserves_persisted_approvals_reviewer`
- Clippy for the affected crates
## Why
Macrobenchmarks benefit from having a way to exercise remote-executor
latency without depending on Docker.
This is a very minimal first cut, if we find that simulating network
conditions is useful we can always expand this scope or switch to a more
robust network shaping approach.
## What
- add a package-local exec-server binary for Cargo and Bazel test
fixtures
- add a host-local WebSocket exec-server fixture and fixed-delay
interposer
- let TestAppServer route its auto environment through that delayed
WebSocket transport
- cover the delayed thread/start path through the public app-server API
## Stack
1. [#31425 test: add TestAppServer
builder](https://github.com/openai/codex/pull/31425)
2. [#31427 test: add delayed exec-server
transport](https://github.com/openai/codex/pull/31427)
3. [#31295 bench: add cold skill load
macrobenchmark](https://github.com/openai/codex/pull/31295)
4. [#31428 bench: add e2e benchmark
entrypoints](https://github.com/openai/codex/pull/31428)
5. [#31429 ci: smoke Bazel e2e
benchmarks](https://github.com/openai/codex/pull/31429)
## Why
Auto-review performance is weaker because of confusing instructions
about sandbox permissions, and because it is given many tools which are
irrelevant to it.
## What
* Update the auto review prompt
* Remove the permissions_instructions developer message
* Only pass exec_tool and view_image tool to the reviewer
## Validation
`just fmt`
`cargo test -p codex-core --lib --quiet`
## Summary
Codex Apps file parameters are exposed to the model as local paths,
uploaded at execution time, and rewritten into provided-file payloads
before the MCP tool call.
The rewrite currently forwards two internal upload fields, `uri` and
`file_size_bytes`, even though they are not part of the documented app
file-reference shape. Strict app schemas can reject those extra fields
before execution.
## Changes
- Stop copying `uri` and `file_size_bytes` into app-facing MCP
arguments.
- Keep the internal `UploadedOpenAiFile` result unchanged.
- Preserve the existing `download_url`, `file_id`, `mime_type`, and
`file_name` behavior for scalar and array file inputs.
- Verify the MCP invocation and post-tool hook receive exactly the
documented four-field payload against an `additionalProperties: false`
schema.
This intentionally does not add schema inspection or change how
`openai/fileParams` names are discovered.
## Validation
- `just test -p codex-core mcp_openai_file` (6 passed)
- `just test -p codex-core codex_apps_file_params_` (2 passed)
- `just fix -p codex-core`
- `just fmt`
- `git diff --check`
### summary
We want to pause code-mode from yielding back to the model when a
subcommand triggers an approval prompt. This means that all of these
previously inline blocking requests should also take out a
ElicitationService registration.
This also does some plumbing refactoring to request patch approval to
make it match the other `request_*_approval` methods in that it blocks
on the approval in the function instead of returning the oneshot
channel, this affords our ability to encapsulate the ElicitationService
registration via RAII.
Adds tests to confirm the blocking behavior for code_mode both in suite
tests and that the session holds them.
## 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.
## 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`
## 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.
## 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
`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.
## 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.
## 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.
## 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
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.
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)
## Summary
- enable `auth_elicitation` by default
- promote the feature to `Stable`, as default-enabled features must be
stable
- update the feature regression test to cover the new lifecycle state
and default
## Impact
Auth elicitation is now available without requiring users or clients to
opt in through configuration.
## Testing
- `just test -p codex-features` (52 passed)
## Why
Approval guidance is currently assembled entirely by the client. Model
Messages V2 needs model catalogs to provide model-specific `on_request`
guidance for both user-reviewed and auto-reviewed approval flows while
retaining the existing generated prompt as a compatibility fallback.
## What changed
- add nullable `on_request` and `on_request_auto_review` catalog
messages
- select the message matching the active approvals reviewer for
`on_request` policies
- replace the complete legacy approval section when the selected catalog
value exists, including support for an empty string that suppresses the
section
- retain legacy rendering when the object or selected key is absent, and
for non-`on_request` policies
- preserve approval messages when base-instruction or personality
overrides clear instruction templates
- refresh permissions instructions when the active model changes
- pass catalog messages through initial and incremental permissions
construction
## Relationship to reviewer persistence
PR #31309 independently persists the approvals reviewer in turn context
and refreshes permissions when that reviewer changes. This PR is based
directly on `main` and does not duplicate that rollout migration; once
both land, reviewer switches will also select and append the new catalog
variant.
## Testing
- `just test -p codex-protocol`
- `just test -p codex-prompts`
- `just test -p codex-models-manager`
- `just test -p codex-core permissions_messages`
## Summary
- Adds `writes` to `AppToolApproval` and exposes it through config and
app-server schemas, including
`[apps._default].default_tools_approval_mode`.
- In `writes`, tools with `readOnlyHint = true` skip approval; all other
tools prompt, including non-destructive writes and tools without
annotations.
- Prevents session or persistent approval choices in this mode so later
writes still prompt.
## Why
`auto` only prompts for risk-hinted actions, while `prompt` also
interrupts reads. Apps need a middle mode that gates writes without
prompting for declared read-only actions.
## Validation
- `just write-config-schema`
- `just write-app-server-schema`
- `just fmt`
- `just test -p codex-core mcp_turn_metadata` (4 passed)
- `just test -p codex-core writes_mode` (2 passed)
- `just test -p codex-app-server config_read_includes_apps` (1 passed)
- `just test -p codex-app-server-protocol` (251 passed)
- `just test -p codex-config` (200 passed)
- `just test -p codex-cli` (300 passed)
- `just fix -p codex-core -p codex-config -p codex-app-server-protocol
-p codex-app-server -p codex-cli`
This PR depends on [#31296](https://github.com/openai/codex/pull/31296)
for the canonical-to-legacy event mappings.
## Description
This PR makes dynamic tools emit canonical `TurnItem::DynamicToolCall`
lifecycle instead of `DynamicToolCallRequest` /
`DynamicToolCallResponse` directly.
App-server v2 now sends the client `DynamicToolCall` request from the
canonical item start. It ignores the mapped legacy request/response
events, so clients receive one item start and one tool request.
## Why
Dynamic tools are a separate migration boundary because their start
event also drives a client request. Keeping that routing change isolated
makes it easier to verify that the request still happens exactly once.
## What changed
- Emit in-progress and completed/failed dynamic tool items from the
dynamic tool handler.
- Move app-server client request dispatch onto canonical dynamic item
starts.
- Add focused app-server coverage for the canonical start notification
and client request.