## Why
Admins need persistent defaults for the model, reasoning effort, and
service tier shown when the Desktop App creates a new thread. These are
initialization defaults rather than runtime constraints: the App should
use them to initialize its draft while still allowing a user to make an
explicit selection.
The app-server therefore needs to expose the managed values before
thread creation without changing `thread/start` behavior for other
clients.
## What changed
- Parse `model`, `model_reasoning_effort`, and `service_tier` from
`[models.new_thread]` in `requirements.toml`.
- Compose the `models` requirements through the existing
requirements-layer precedence rules.
- Expose the resolved values through `configRequirements/read` as
`requirements.models.newThread`.
- Add the corresponding app-server protocol types and regenerate the
JSON and TypeScript schema fixtures.
- Document the new `configRequirements/read` fields in the app-server
README.
## Scope
This PR is data plumbing only. It does not apply these values during
`thread/start` and does not change thread creation for existing
app-server clients, resumed or forked sessions, internal or subagent
sessions, `codex exec`, or the TUI. A companion Desktop App change owns
draft initialization, sends the effective settings for ordinary and
prewarmed starts, and preserves explicit user changes.
## Validation
- Requirements deserialization coverage for `[models.new_thread]`
- Requirements-layer precedence coverage
- App-server API mapping coverage
- `configRequirements/read` integration coverage
- Regenerated app-server JSON and TypeScript schema fixtures
## Description
This PR adds a new `historyMode = "legacy" | "paginated"` to `Thread`.
This will be stored in `SessionMeta` in the JSONL rollout file and as a
new column in the SQLite thread_metadata table, and exposed on
`thread/start` and on the `Thread` object in app-server.
## What changed
- Added canonical `ThreadHistoryMode` with `legacy` and `paginated`,
defaulting old and new SessionMeta to `legacy`.
- Carried `history_mode` through core session config, ThreadStore stored
metadata, local/in-memory stores, rollout metadata extraction, and the
existing SQLite `threads` table.
- Added experimental `historyMode` to app-server v2 `Thread` and
`thread/start`.
- Made paginated stored threads metadata-discoverable but unsupported
for legacy full-history reads, `load_history`, live resume, and create
paths.
- Regenerated app-server schema fixtures and added
protocol/state/thread-store/app-server coverage for persistence and
fail-closed behavior.
## Compatibility floor
Because users may be running various versions of Codex binaries on the
same machine (TUI, Codex App, etc.), we will need to establish a
compatibility floor for upcoming paginated threads, which will change
how thread storage reads and writes work.
The overall plan here:
```
Release N:
- Add historyMode to SessionMeta / Thread / SQLite metadata.
- Teach binaries to understand paginated threads.
- If a binary sees `historyMode="paginated"` but does not support the paginated contract, it refuses to resume/mutate the thread.
- Default remains `"legacy"`.
Release N+1:
- First-party clients start opting into paginated threads where appropriate.
- Internal dogfood / staged rollout.
- Measure old-client usage and paginated-thread unsupported errors.
Release N+2:
- Only after Release N+ is overwhelmingly deployed, make paginated the default.
- Accept that a small tail of N-1-or-older binaries may not understand paginated threads.
```
The important behavior change is fail-closed handling for a binary that
encounters a persisted `paginated` thread before it knows how to fully
support paginated history. In app-server, if a thread is `paginated`, we
will:
- allow metadata-only discovery paths like `thread/list` and
`thread/read(includeTurns=false)`, so clients can still see the thread
and inspect its `historyMode`
- reject legacy full-history/live-thread paths like
`thread/read(includeTurns=true)` and `thread/resume` with an unsupported
JSON-RPC error
- avoid silently treating an unknown or future `historyMode` as `legacy`
Under the hood, the ThreadStore layer also rejects legacy operations
that would need to load or replay the full thread history for a
paginated thread. That gives us the behavior we want for Release N:
future paginated threads are visible, but this binary fails closed
instead of trying to operate on them as if they were legacy threads.
## Why
MCP tool-call events need to expose trusted app identity and action
metadata directly so v2 clients do not have to infer it from tool names
or resource URIs.
## What changed
- Add optional `appName`, `templateId`, and `actionName` fields to MCP
tool-call `appContext`.
- Populate `appName` and `templateId` from trusted Codex Apps metadata,
and derive `actionName` from the trusted app resource metadata.
- Preserve all three fields through core events, legacy protocol events,
persisted thread history, resume redaction, and app-server v2 responses.
- Document the public `appContext` fields in
`codex-rs/app-server/README.md`.
- Regenerate app-server JSON and TypeScript schemas and add coverage for
serialization, persistence, redaction, and metadata propagation.
## Validation
- `just test -p codex-app-server-protocol mcp_tool_call`
- `just test -p codex-core
mcp_tool_call_item_metadata_only_trusts_codex_apps_identity
mcp_tool_call_item_includes_app_identity`
- `just write-app-server-schema`
---------
Co-authored-by: Martin Au-Yeung <280153141+martinauyeung-oai@users.noreply.github.com>
## Summary
- distinguish expired, non-refreshable stored MCP OAuth credentials from
first-time missing credentials
- carry a typed `failureReason: "reauthenticationRequired"` on the
existing `mcpServer/startupStatus/updated` notification only when user
action is required
- keep the public MCP auth-status API unchanged and regenerate the
app-server protocol schemas and documentation
## Why
An MCP server with an expired access token and no usable refresh token
currently fails startup without giving clients a reliable, typed
recovery signal.
The existing startup-status notification is the natural place to carry
this state. Its nullable `failureReason` keeps the recovery reason
attached to the failed startup transition without adding a one-off
notification. Internally, Codex distinguishes first-time login from
reauthentication and emits the reason only when the startup error itself
requires authentication.
## User impact
App clients can prompt an existing user to reconnect an MCP server when
automatic recovery is impossible by handling a failed
`mcpServer/startupStatus/updated` notification whose `failureReason` is
`reauthenticationRequired`. Starting, ready, cancelled, unrelated
failures, and first-time setup carry no reauthentication reason.
## Companion app PR
- openai/openai#1069582
## Validation
- `just test -p codex-app-server-protocol` — 248 passed; schema fixture
tests passed
- `cargo check -p codex-app-server -p codex-tui`
- `just test -p codex-rmcp-client -p codex-mcp` — 184 passed, 2 skipped
- `just test -p codex-protocol -p codex-app-server-protocol -p
codex-mcp` — 579 passed
- `just write-app-server-schema`
- `just fmt`
## Why
Helper threads such as task title generation can request a model ID that
is valid for the default OpenAI provider but unavailable from the active
provider. With Amazon Bedrock, `gpt-5.4-mini` is rejected while the
provider static catalog exposes Bedrock model IDs such as
`openai.gpt-5.5` and `openai.gpt-5.4`. This causes repeated background
404s and can surface a misleading turn error even when the main turn
succeeds.
Clients need an explicit way to ask app-server to resolve an unavailable
helper model to the active provider default. That fallback must remain
limited to providers with an authoritative static catalog so custom or
dynamically discovered model IDs are not rewritten based on an
incomplete catalog.
Fixes#28741.
## What changed
- Add the experimental `allowProviderModelFallback` option to
`thread/start`, defaulting to `false` to preserve existing behavior.
- Thread the option through thread creation and model selection.
- When enabled for a static model manager, preserve requested models
present in the catalog and replace unavailable models with the provider
default.
- Continue preserving explicit model IDs for dynamic model managers
without fetching a catalog solely to validate them.
- Document the new `thread/start` behavior in the app-server API
overview.
## Test
Temporary test-client harness:
```
ThreadStartParams {
model: Some("gpt-5.4-mini".to_string()),
allow_provider_model_fallback: true,
..Default::default()
}
```
Command:
```
CODEX_HOME=/tmp/codex-bedrock-thread-start-home \
CODEX_E2E_BEDROCK_THREAD_START_ONLY=1 \
./target/debug/codex-app-server-test-client \
--codex-bin ./target/debug/codex \
-c 'model_provider="amazon-bedrock"' \
send-message-v2 --experimental-api ignored
```
Relevant output:
```
> "method": "thread/start",
> "params": {
> "model": "gpt-5.4-mini",
> "modelProvider": null,
> "allowProviderModelFallback": true,
> ...
> }
< "result": {
< "model": "openai.gpt-5.5",
< "modelProvider": "amazon-bedrock",
< ...
< }
```
## Why
#28522 routes selected-plugin HTTP MCP traffic through the owning
executor, but OAuth bootstrap and refresh still used host-local clients.
Executor-only servers therefore cannot complete discovery or login
through the same network boundary as the MCP connection.
## What changed
- adapt `codex_exec_server::HttpClient` to RMCP 1.8's `OAuthHttpClient`
contract
- let RMCP own discovery, dynamic registration, PKCE, token exchange,
and refresh
- route auth status, persisted-token startup, and app-server login
through the server runtime while preserving the existing local discovery
path
- add optional `threadId` to `mcpServer/oauth/login` and echo it in the
completion notification
- implement RMCP's redirect policy and 1 MiB OAuth response limit over
executor HTTP
- cover selected-thread OAuth discovery and login through an
executor-only route
Depends on #28522.
## Why
Ultra should be one user-facing reasoning selection for work that
benefits from both maximum reasoning and proactive multi-agent
delegation. Without it, clients must coordinate maximum reasoning with
the experimental `multiAgentMode` setting, even though the inference
backend still expects its existing `max` effort value.
This change makes reasoning effort the source of truth: clients select
`ultra`, core derives proactive multi-agent behavior when the turn is
eligible for multi-agent V2, and inference requests continue to use the
backend-compatible `max` value.
## What changed
- Add `ultra` as a first-class reasoning effort and preserve
model-catalog ordering when exposing it to clients.
- Convert `ultra` to `max` at the inference request boundary, including
Responses HTTP/WebSocket requests, startup prewarm, compaction, and
memory summarization.
- Derive effective multi-agent mode per turn from effective reasoning
effort:
- eligible multi-agent V2 + `ultra` → `proactive`
- eligible multi-agent V2 + any other effort → `explicitRequestOnly`
- V1 or otherwise ineligible sessions → no multi-agent mode instruction
- Keep the derived effective mode in turn context history so successive
turns can emit a developer-message update only when the effective mode
changes.
- Remove selected multi-agent mode from core session configuration, turn
construction, thread settings, resume/fork restoration, and subagent
spawn plumbing. Subagents inherit reasoning effort and derive their own
effective mode.
- Retain the experimental app-server `multiAgentMode` fields for wire
compatibility while marking them deprecated. Request values are accepted
but ignored; compatibility response fields report `explicitRequestOnly`.
- Display Ultra in the TUI using the order supplied by `model/list`.
## Validation
- `just test -p codex-core ultra_reasoning_uses_max_for_requests`
- `just test -p codex-tui model_reasoning_selection_popup`
## Why
`WorldState` currently remembers its model-visible diff baseline only in
memory. That leaves no durable source for restoring the exact baseline
after resume, fork, rollback, or compaction.
This is the second PR in the WorldState persistence stack, built on
#29833 and following #29249. It records durable state transitions; the
next PR will replay them during rollout reconstruction.
## What
- Add a `world_state` rollout item containing either a full snapshot or
an RFC 7386 JSON Merge Patch.
- Persist a full snapshot after initial context and after compaction
establishes a new context window.
- Persist non-empty patches when later sampling steps or turns advance
the WorldState baseline.
- Write model-visible history before its matching WorldState record, so
an interrupted write can only cause a safe repeated update on replay.
- Preserve WorldState records for full-history forks while excluding
them from thread previews, metadata, and app-server history
materialization.
Older binaries read rollout lines independently, so they skip the
unknown `world_state` records while retaining the rest of the thread.
## Testing
- `just test -p codex-core
snapshot_merge_patch_changes_and_removes_nested_values`
- `just test -p codex-core
world_state_baseline_deduplicates_until_history_is_replaced`
- `just test -p codex-core
deferred_executor_compaction_preserves_then_updates_environment_once`
- `just test -p codex-protocol`
- `just test -p codex-rollout`
- `just test -p codex-state`
- `just test -p codex-thread-store`
- `just test -p codex-app-server-protocol`
## Summary
- Add `iconAssets` and `iconDarkAssets` to the app-list protocol.
- Preserve structured icons through directory merging and the connector,
app-
server, and TUI boundaries.
- Keep legacy logo URLs unchanged as compatibility fallbacks.
- Update generated protocol schemas and TypeScript types.
## Why
`thread/list` can filter direct children with `parentThreadId`, but
clients cannot request an entire spawned subtree. Discovering every
descendant requires repeated client-side requests and gives up the
database's existing filtering and pagination path.
## What changed
Experimental clients can use `ancestorThreadId` to return strict
descendants at any depth while `parentThreadId` retains its direct-child
meaning. The filters are mutually exclusive, the ancestor is excluded,
and every result preserves its immediate `parentThreadId` so callers can
reconstruct the tree.
## How it works
- **Explicit relationship:** Internal list parameters distinguish direct
children from transitive descendants without changing the meaning of
`parentThreadId`.
- **Existing graph:** Persisted parent-child spawn edges remain the
source of truth, so descendant lookup needs no schema migration or
ancestry cache.
- **Indexed traversal:** A recursive SQLite query starts from the
parent-edge index, walks each generation, and applies thread filters,
sorting, and cursor pagination in the same database request.
- **Reconstructable results:** The response stays flat and normally
ordered while carrying each descendant's immediate parent.
## Verification
Ran 550 tests across the protocol, state, rollout, and thread-store
crates, then reran the four focused state, store, and app-server
descendant-listing tests after the final diff reduction. Scoped Clippy
and formatting checks passed. Stable and experimental schema generation
was checked; the stable fixtures remain unchanged while the experimental
schema includes the new field.
## What changed
- Show per-type import counts in the `/import` review UI and started
message.
- Render completion results as a multi-line summary with total
imported/failed counts and one row per import type.
- Add snapshot coverage for the updated review and completion output.
<img width="537" height="322" alt="Screenshot 2026-06-23 at 9 41 20 PM"
src="https://github.com/user-attachments/assets/166542eb-2097-4b2b-8130-8f6fd8c680ce"
/>
## Why
The TUI previously only reported that Claude Code import started or
finished. Users could not see how many items of each type were selected
or how many actually imported versus failed.
## Why
Inter-agent messages are recorded in live history as
`ResponseItem::AgentMessage`, but rollouts stored
`InterAgentCommunication` and rebuilt the response item during resume.
This made the rollout differ from the actual Responses history.
## What changed
- store the prepared `agent_message` response item directly
- keep `trigger_turn` in a small local metadata record for fork
truncation
- keep reading older `inter_agent_communication` rollout items
## Summary
- rename the rollout-budget exhaustion error from
`RolloutBudgetExceeded` to `SessionBudgetExceeded`
- expose the matching app-server v2 wire value as
`sessionBudgetExceeded`
- regenerate JSON/TypeScript schema fixtures and update the app-server
docs and focused tests
This is a naming-only follow-up to #29715 based on [Pavel's review
suggestion](https://github.com/openai/codex/pull/29715#discussion_r3463183480).
Runtime behavior is unchanged.
## Tests
- `just test -p codex-core rollout_budget`
- `just test -p codex-app-server-protocol`
- `just fmt`
- `just write-app-server-schema`
## Why
The app-server and exec-server expose separate JSON-RPC APIs, but
exec-server currently sources its serialized protocol and envelope types
through app-server-oriented code. Giving each API an explicit owner
makes the crate boundary legible without introducing shared generic
envelopes.
## What changed
- Added `codex-exec-server-protocol` to own exec DTOs, process IDs, and
JSON-RPC envelopes.
- Updated exec-server clients, transports, handlers, and tests to use
the new crate.
- Exposed app-server's existing JSON-RPC types through a public `rpc`
module while retaining root re-exports.
- Preserved existing wire shapes, including exec `PathUri` behavior.
## Stack
This is PR 1 of 6. Next: [PR
#29721](https://github.com/openai/codex/pull/29721), which moves auth
mode below the app wire boundary.
## Validation
- Exec-server protocol and server coverage passed in the focused
protocol test runs.
- App-server protocol schema fixtures passed.
## Summary
- surface shared rollout-budget exhaustion as
`CodexErr::RolloutBudgetExceeded` instead of a generic interrupted turn
- map it through the existing `CodexErrorInfo` and app-server v2
`codexErrorInfo` path
- keep local compaction from retrying after the shared rollout budget is
exhausted
This gives app-server clients a stable `rolloutBudgetExceeded` error
they can classify without guessing from `status="interrupted"`.
## Tests
- `just test -p codex-core rollout_budget`
## Description
Rename the experimental app-server item pagination API from
`thread/turns/items/list` to `thread/items/list` and make `turnId`
optional. Clients can now page persisted items across a thread, or still
filter to one turn when needed.
## What changed
- Rename the request/response protocol types and JSON-RPC method to
`ThreadItemsList*` / `thread/items/list`.
- Pass optional `turnId` through to `ThreadStore::list_items`.
- Update app-server docs and focused protocol/app-server tests.
## Validation
- `just test -p codex-app-server-protocol thread_items_list_round_trips`
- `just test -p codex-app-server thread_items_list_returns_unsupported`
## Why
view_image needs to support foreign OS remote executors.
## What
- resolve image paths against the selected environment as `PathUri` and
read them through that environment's filesystem
- keep app-server's public path field wire-compatible as
`LegacyAppPathString`, with purpose-specific UI rendering
- cover relative and absolute target-native paths in the core
integration test and run the full `view_image` suite under wine-exec
without skips
## Summary
- read the request-scoped safety-buffering treatment from HTTP response
headers and per-turn WebSocket metadata through one shared header parser
- combine that treatment with Responses API safety-buffering signals
- propagate `showBufferingUi` and nullable `fasterModel` through the
existing `model/safetyBuffering/updated` app-server notification
- update the app-server documentation and generated JSON and TypeScript
schemas
The public implementation contains no model mapping or real model
identifier. Tests and protocol examples use generic `current-model` and
`faster-model` placeholders only.
## Dependencies
- server-side treatment evaluation:
https://github.com/openai/openai/pull/1060247
- initial Responses API safety-buffering propagation:
https://github.com/openai/codex/pull/29371
- Codex App UI: https://github.com/openai/openai/pull/1057789
## Validation
- Codex API tests: 129 passed
- focused Codex core safety-buffering integration test passed
- app-server protocol tests passed after regenerating schema fixtures
- Clippy fix and repository formatting completed successfully
The broader app-server run compiled all changed crates and completed
with 1,269 passing tests. Its remaining failures were unrelated
environment limitations: macOS sandbox application was denied, one
expected test binary was unavailable, and several existing subprocess
tests timed out as a result.
Adds additive dark-mode plugin logo metadata across manifests, remote
catalogs, and the app-server protocol while keeping uninstalled Git
listings free of synthetic local paths.
Supersedes #28945. This replacement uses an upstream branch so trusted
CI can use the repository-provided remote Bazel configuration.
## Current state
Plugin interfaces expose only the default logo asset. Clients therefore
cannot select a dedicated dark-mode logo even when a plugin provides
one.
## What this PR changes
- Adds nullable `logoDark` and `logoUrlDark` fields to
`PluginInterface`.
- Resolves local `interface.logoDark` assets and maps remote
`logo_url_dark` values.
- Removes path-backed interface assets, including `logoDark`, from
uninstalled Git fallback listings until the plugin has a real local
root.
- Updates the bundled plugin validator and manifest reference.
- Regenerates the app-server JSON schemas and TypeScript types.
Local manifests expose `interface.logoDark` as a package-relative asset
path. Remote catalog responses expose `logo_url_dark`. These values map
into separate app-server fields so clients can preserve local-path and
remote-URL handling.
## Risk
The fields are additive and nullable, so existing clients retain their
current logo behavior. The main risks are an incomplete mapping path or
exposing a synthetic local path for an uninstalled Git plugin.
Local-manifest, remote-catalog, fallback-listing, protocol
serialization, and app-server integration tests cover those paths.
Spiciness: 2/5
## Testing
- `just write-app-server-schema`
- `just fmt`
- Regression test first failed with `logo_dark` resolved to
`/assets/logo-dark.png`, then passed after the fallback-listing fix.
- `just test -p codex-core-plugins` (267 tests passed)
- `just test -p codex-app-server 'suite::v2::plugin'` (114 tests passed)
- `just test -p codex-app-server-protocol -p codex-core-plugins -p
codex-plugin -p codex-skills` (517 tests passed before the follow-up)
- `just test -p codex-tui plugin` (47 tests passed)
- Validated a local plugin manifest containing `interface.logoDark` with
the bundled validator.
## Manual verification
Create a local plugin with both `interface.logo` and
`interface.logoDark`, then call `plugin/list` or `plugin/read`. Confirm
the response contains separate `logo` and `logoDark` paths. For a remote
catalog entry, confirm `logoUrlDark` is populated from `logo_url_dark`.
For an uninstalled Git marketplace entry, confirm path-backed interface
assets remain absent until installation.
Issue: N/A - coordinated maintainer change.
## Why
`permissionProfile/list` currently advertises every built-in and
configured profile even when effective enterprise requirements prevent
selecting it. That forces each client to reconstruct policy from
lower-level requirement fields, which is easy to miss and difficult to
keep consistent.
The catalog should remain complete so clients can explain that an option
was disabled by an administrator, while also reporting whether each
profile is selectable.
## What
- Add an `allowed` field to each permission profile summary.
- Build a shared catalog from the effective config and current
requirements, including `allowed_sandbox_modes`, `allowed_permissions`,
and filesystem restrictions.
- Use the shared catalog in app-server and the TUI so disallowed
profiles remain visible but cannot be selected.
- Use the canonical `:danger-full-access` profile ID in the TUI.
- Update the app-server schemas, API documentation, behavioral tests,
and TUI snapshots.
## Scope
This PR targets `main` directly and is independent of #24852. It
preserves the current behavior where built-in profiles are constrained
by sandbox-mode requirements and `allowed_permissions` applies to
configured profiles.
## Testing
- `just test -p codex-core
permission_profile_catalog_marks_profiles_disallowed_by_requirements`
- `just test -p codex-app-server permission_profile_list`
- `just test -p codex-app-server-protocol`
- `just test -p codex-tui profile_permissions`
- `just fix -p codex-core`
- `just fix -p codex-app-server-protocol`
- `just fix -p codex-app-server`
- `just fix -p codex-tui`
- `just fmt`
---------
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Joey Trasatti <joey.trasatti@openai.com>
# Summary
Codex required every ChatGPT account to have an email address. A
service-account personal access token can return valid account metadata
without one, so PAT login failed while decoding the metadata response.
This change makes email optional in the account metadata type that owns
it and preserves that absence through authentication, provider account
state, the app-server API, generated clients, and TUI bootstrap.
Existing accounts with email addresses keep the same behavior.
## Behavior-changing call sites
| Call site | Behavior after this change |
| --- | --- |
| `login/src/auth/personal_access_token.rs` | PAT metadata accepts a
missing or null email and retains `None`. |
| `agent-identity/src/lib.rs` | Agent Identity JWT claims accept an
omitted email. |
| `login/src/auth/storage.rs` and `login/src/auth/agent_identity.rs` |
Stored and managed Agent Identity records carry `Option<String>`.
Deserialization maps the legacy empty-string sentinel to `None`. |
| `login/src/auth/manager.rs` | `get_account_email` returns the stored
option, and managed identity bootstrap no longer converts `None` to an
empty string. |
| `model-provider/src/provider.rs` and `protocol/src/account.rs` | A
ChatGPT provider account requires a plan type but may carry no email. |
| `app-server-protocol/src/protocol/v2/account.rs` | `account/read`
keeps the `email` field on the wire and returns `null` when the account
has no email. Generated TypeScript and JSON schemas describe a required,
nullable field. |
| `sdk/python/src/openai_codex/generated/v2_all.py` | The generated
Python `ChatgptAccount` model accepts `None` for email. |
| `tui/src/app_server_session.rs` | Email-less ChatGPT accounts
bootstrap normally, keep external feedback routing, omit account-email
telemetry, and display the plan in account status. |
## Design decisions
- Missing email remains `None` at every layer. The code never uses an
empty string as a substitute.
- The app-server response includes `"email": null` instead of omitting
the field. Clients retain a stable response shape.
- Plan type remains required for provider account state. This change
relaxes only the email assumption.
## Testing
Tests: affected test targets compile, scoped Clippy and formatting pass,
a focused TUI snapshot covers plan-only account status, real
before/after PAT login smoke covers metadata without email, app-server
smoke covers `account/read` with `email: null`, and a regression smoke
covers an existing email-bearing PAT. Unit tests run in CI.
## Evidence
Visual smoke evidence will be attached here.
## Description
This PR cuts Codex over from generic `ResponseItem.metadata` (introduced
here: https://github.com/openai/codex/pull/28355) to
`ResponseItem.internal_chat_message_metadata_passthrough`, which is the
blessed path and has strongly-typed keys.
For now we have to drop this MAv2 usage of `metadata`:
https://github.com/openai/codex/pull/28561 until we figure out where
that should live.
## Why
Multi-agent delegation policy was split across `multiAgentMode`,
`features.multi_agent_mode`, and `usage_hint_enabled`. These controls
could disagree: a requested mode could be downgraded by the feature
flag, and disabling usage hints also disabled mode instructions.
Some clients also need multi-agent tools without adding
delegation-policy text to model context. The previous two-mode API could
not express that directly.
## What changed
`multiAgentMode` is now the only live delegation-policy control:
| Mode | Behavior |
| --- | --- |
| `none` | Keep multi-agent tools available without adding mode
instructions. |
| `explicitRequestOnly` | Only delegate after an explicit user request.
|
| `proactive` | Delegate when parallel work materially improves speed or
quality. |
- new threads default to `explicitRequestOnly`; omitting the mode on
later turns keeps the current value
- thread start, resume, fork, and settings responses always report the
concrete current mode instead of `null`
- mode selection remains sticky across turns and resume
- usage-hint text no longer controls whether mode instructions apply
- `features.multi_agent_mode` and `usage_hint_enabled` remain accepted
as ignored compatibility settings so existing configs continue to load
- app-server documentation and generated schemas describe the three-mode
API
## Tests
- `just test -p codex-core multi_agent_mode`
- `just test -p codex-core multi_agent_v2_config_from_feature_table`
- `just test -p codex-core spawn_agent_description`
- `just test -p codex-features`
- `just test -p codex-app-server-protocol`
- `just test -p codex-app-server multi_agent_mode`
Responses API safety buffering metadata currently stops at the transport
boundary, so app-server clients cannot render the in-progress safety
review state.
This change:
- decodes and deduplicates `safety_buffering` metadata from Responses
API SSE and WebSocket events without suppressing the original response
event
- emits a typed core event containing the requested model plus backend
use cases and reasons
- forwards that event as `turn/safetyBuffering/updated` through
app-server v2 and updates generated protocol schemas
- keeps the side-channel event out of persisted rollouts and turn timing
This supports the Codex Apps buffering UX and depends on the Responses
API backend work in https://github.com/openai/openai/pull/1044569 and
https://github.com/openai/openai/pull/1044571.
Validation:
- focused `codex-core` safety-buffering integration test passes
- `cargo check -p codex-core -p codex-app-server -p
codex-app-server-protocol`
- `just fix -p codex-api -p codex-protocol -p codex-core -p
codex-app-server-protocol -p codex-app-server -p codex-rollout -p
codex-rollout-trace -p codex-otel`
- `just fmt`
- broad package test run: 4,430/4,492 passed; 62 unrelated
local-environment/concurrency failures involved unavailable test
binaries, MCP subprocess setup, and app-server timeouts
## Why
The rendered `<token_budget>` fragment identifies the thread and current
context window, but it does not expose enough lineage to identify the
first window in the thread or the immediately preceding window. Those
IDs also need to remain stable across compaction, resume, and rollback.
## What changed
- Track first, previous, and current UUIDv7 context-window IDs in
auto-compaction state.
- Render `thread_id`, `first_window_id`, `previous_window_id`, and the
current window ID in the full `<token_budget>` fragment.
- Persist the first and previous window IDs in compacted rollout
checkpoints and restore them during rollout reconstruction.
- Preserve compatibility with older compacted records that do not
contain the new optional fields.
- Update focused state, rendering, reconstruction, rollback, and
serialization coverage.
## Validation
- `just test -p codex-core token_budget`
- `just test -p codex-protocol compacted_item::tests`
- `just test -p codex-core tracks_prefill_and_window_boundaries`
- `just test -p codex-core
reconstruct_history_uses_replacement_history_verbatim`
- `just test -p codex-core
thread_rollback_restores_cleared_reference_context_item_after_compaction`
## Why
Once multi-agent mode can be selected per turn, clients also need to
choose the initial selection when creating a thread and observe that
selection through lifecycle and settings APIs.
The selected value is intentionally distinct from the effective
model-visible value: no client selection is represented as `null`, even
though an eligible multi-agent v2 turn derives `explicitRequestOnly` as
its effective default.
## What changed
- Add the optional experimental `thread/start.multiAgentMode` parameter
and pass it through thread creation.
- Preserve an omitted initial value as an unset selection rather than
eagerly storing `explicitRequestOnly`.
- Apply an explicit `thread/start` selection to the first turn through
the session configuration established at thread creation.
- Restore the latest persisted effective mode as the selected baseline
on cold resume when rollout history contains one.
- Inherit the optional selected mode from a loaded parent when creating
related runtime threads.
- Return the current selected `multiAgentMode` from `thread/start`,
`thread/resume`, `thread/fork`, and thread settings, using `null` when
no mode is selected.
- Keep lifecycle reporting independent from model capability and feature
eligibility; core turn construction remains responsible for calculating
and persisting the effective mode.
## Not covered
- Clearing an existing loaded-session selection back to unset through
`turn/start`; omitted or `null` currently retains the session's
selection.
- A TUI control, slash command, or `config.toml` preference.
## Verification
- `CARGO_INCREMENTAL=0 just test -p codex-app-server-protocol`
- `CARGO_INCREMENTAL=0 just test -p codex-app-server multi_agent_mode`
The focused app-server coverage verifies explicit `thread/start`
initialization, first-turn prompting, nullable reporting for an omitted
selection, and retention of selections that are not currently
runtime-eligible.
## Stack
Stacked on #28685. This PR contains only the thread initialization and
lifecycle/settings API layer.
## Why
Multi-agent v2 currently carries an explicit-request-only delegation
rule in its static usage hint. That provides a safe default, but it
prevents clients from selecting proactive delegation per turn without
changing static guidance or rewriting prior model context.
This change makes delegation mode a session selection that can be
updated through `turn/start`, while deriving the effective model-visible
mode separately for each turn. Eligible multi-agent v2 turns remain
explicit-request-only unless proactive mode is both selected and
enabled.
## What changed
- Add the experimental `turn/start.multiAgentMode` parameter with
`explicitRequestOnly` and `proactive` values. Omission retains the
loaded session's current optional selection.
- Add the default-off `features.multi_agent_mode` feature gate. Eligible
multi-agent v2 turns use the selected mode when enabled; an unset
selection or disabled gate resolves to `explicitRequestOnly`.
- Treat mode prompting as inapplicable for multi-agent v1 and other
unsupported session configurations, producing no multi-agent mode
developer message rather than rejecting the turn.
- Move the explicit-request-only rule out of the static v2 usage hint
and into a bounded, tagged developer context fragment.
- Emit the effective mode in initial context and only when that
effective mode changes on later turns.
- Persist the effective mode in `TurnContextItem` as the durable
baseline for resume and context-update comparisons.
Historical rollout items are not rewritten. Later mode developer
messages establish the current rule incrementally.
## Not covered
- Initial selection through `thread/start` and selected-mode reporting
from thread lifecycle/settings APIs; those are isolated in the stacked
#28792.
- A TUI control or slash command for selecting the mode.
- Persisting a preferred mode to `config.toml`; selection remains
session/turn scoped.
- Changes to multi-agent concurrency limits, tool availability, or model
catalog capability declarations.
- Rewriting historical rollout prompt items. Cold resume restores the
latest persisted effective mode when available while leaving historical
developer messages intact.
## Verification
- `CARGO_INCREMENTAL=0 just test -p codex-core multi_agent_mode`
- Focused app-server coverage verifies that `turn/start.multiAgentMode`
produces proactive developer instructions for an eligible v2 turn.
## Stack
Followed by #28792, which adds `thread/start` initialization and
lifecycle/settings observability.
## Why
Remote environments registered through `environment/add` currently use
the fixed 10-second WebSocket connection timeout. Slow-starting
executors need a caller-selected connection window, but this should not
add retry policy or couple exec-server behavior to Core’s
`deferred_executor` feature.
Make the timeout an optional part of the existing experimental request.
Existing clients continue using the current default, while callers that
know an executor may take longer can request a larger window explicitly.
Depends on #28683.
## What changed
- Add optional `connectTimeoutMs` to `EnvironmentAddParams` and document
it in the app-server README.
- Pass the optional timeout through `EnvironmentRequestProcessor` into
one `EnvironmentManager::upsert_environment()` path; the manager applies
the existing default when it is omitted.
- Preserve the existing single-attempt lifecycle. The configured value
controls WebSocket connection and handshake time for both initial
connection and later reconnects; initialization retains its separate
timeout.
- Add an app-server integration test that sends the real JSON-RPC
request and verifies a stalled handshake observes the requested timeout.
## Test plan
- `just test -p codex-app-server-protocol`
- `just test -p codex-exec-server`
- `just test -p codex-app-server
environment_add_applies_connect_timeout`
## Rollout
This is additive and does not enable `deferred_executor`. Callers should
send a non-default timeout only after a compatible app-server is
deployed; omitted or `null` values retain the existing 10-second
default.
## Summary
- Remove the realtime `architecture` selector from core protocol,
app-server protocol, config parsing, generated schemas, and callers.
- Always create WebRTC realtime calls with the AVAS query params:
`intent=quicksilver&architecture=avas`.
- Keep direct websocket realtime behavior on the existing config/default
path, while WebRTC starts without an explicit version now default to
realtime v1 because AVAS requires v1.
## Notes
- WebRTC realtime now means AVAS. If a caller explicitly asks to start
WebRTC with realtime v2, Codex rejects that request because the AVAS
WebRTC path only supports realtime v1. Websocket realtime is separate
and can still use realtime v2.
- The old `[realtime] architecture = "realtimeapi" | "avas"` config knob
is removed. Local configs that still set it will need to delete that
line.
- Some app-server tests that were only trying to exercise realtime v2
protocol behavior now use websocket transport, because WebRTC is
intentionally locked to AVAS/v1. Separate WebRTC tests cover the AVAS
query params, v1 startup, SDP flow, and sideband join.
## Validation
- Merged fresh `origin/main` at `83e6a786a2`.
- `just fmt`
- `just write-config-schema`
- `just write-app-server-schema`
- `git diff --check`
- `just test -p codex-api -p codex-core -p codex-app-server-protocol -p
codex-app-server realtime` (176 passed)
- `just test -p codex-protocol -p codex-config` (413 passed)
## Why
The token-budget context currently identifies a context window by its
thread-local sequence number. A UUIDv7 gives the model a stable opaque
identity that remains fixed for a window and rotates when compaction or
`new_context` starts the next one.
## What changed
- Preserve the existing monotonic value as `window_number` and add a
UUIDv7 `window_id` to `CompactedItem`.
- Generate and rotate the UUID with auto-compaction window state,
persist it alongside the number, and reconstruct it on resume and
rollback.
- Accept legacy compacted rollout records where the numeric `window_id`
represented the window number.
- Use the UUID only in token-budget context; existing request headers
and metadata continue using `thread_id:window_number`.
## Testing
- `just test -p codex-protocol compacted_item::tests`
- `just test -p codex-core token_budget`
## Why
Make it possible to load AGENTS.md from remote exec-servers whose OS is
different than app-server.
## What
- keep `AGENTS.md` discovery and provenance as `PathUri`, with
root-aware parent and ancestor traversal
- expose lifecycle instruction sources as legacy app-server path strings
in events while retaining `PathUri` internally
- preserve and test mixed POSIX and Windows paths in model context and
TUI status output
- cover remote Windows loading end to end by seeding the Wine prefix
through host filesystem APIs
- fix bug in `PathUri`'s parent() implementation that would erase
Windows drive letters
## What
Server should request:
```
{
"id": 42,
"method": "currentTime/read",
"params": {
"threadId": "11111111-1111-1111-1111-aaaaafdc2c11"
}
}
```
Client should respond with something like:
```rust
{
"id": 42,
"result": {
"currentTimeAt": 1781717655
}
}
```
## Why
Sessions configured with `clock_source = "external"` need a
thread-specific external time source before inference. The system clock
remains the default production provider.
## Validation
- `cargo test -p codex-app-server-protocol`
- `cargo test -p codex-app-server --test all
current_time_read_round_trip_adds_reminder_to_model_input`
- `cargo test -p codex-app-server
first_attestation_capable_connection_for_thread_only_uses_thread_subscribers`
- `cargo test -p codex-analytics`
- `just fix -p codex-app-server-protocol`
- `just fix -p codex-app-server`
Stacked on #28824.
## Why
App-server must report command events containing foreign-platform paths
without changing existing client or rollout path-string formats.
## What changed
- retain `PathUri` through exec command begin/end events
- convert cwd values to `LegacyAppPathString` at the app-server
compatibility boundary
- drop command actions with foreign paths and log them
- serialize rollout-trace cwd values using their inferred native path
representation
- restore Wine coverage for retained Windows cwd values and successful
completion
## What
Built on the realtime speech-control plumbing merged in #27917.
- Add optional `codexResponseHandoffPrefix` to `thread/realtime/start`.
- Apply that prefix only to automatic V1 commentary sent through
`conversation.handoff.append`; final answers remain unprefixed.
- Add opt-in `clientManagedHandoffs`. When true, core suppresses
automatic response handoffs and completion output so delivery is
controlled by explicit client append APIs.
- Preserve existing automatic behavior by default.
`codexResponsesAsItems: true` continues to select item routing when
client-managed mode is disabled.
## Why
Voice clients need two delivery policies: automatic background context
with silent commentary instructions and fully client-owned handoffs.
Phase-aware prefixing keeps routine commentary silent without
suppressing the final answer, while client-managed mode lets an app
decide exactly which updates to append.
## Validation
- `just fmt`
- `cargo test -p codex-app-server-protocol
serialize_thread_realtime_start`
- `RUST_MIN_STACK=16777216 cargo test -p codex-core --test all
conversation_handoff_persists_across_item_done_until_turn_complete`
- `RUST_MIN_STACK=16777216 cargo test -p codex-app-server --test all
webrtc_v1_client_managed_handoffs_disable_automatic_output`
- `RUST_MIN_STACK=16777216 cargo test -p codex-app-server --test all
webrtc_v1_final_automatic_handoff_omits_silent_prefix`
- `cargo build -p codex-cli --bin codex`
- Local Codex Apps compatibility check: 43 focused webview tests passed,
and a live voice session routed through the source-built app-server.
The explicit `RUST_MIN_STACK` avoids a macOS Tokio test-worker stack
overflow seen with the default test environment.
## Summary
- Track plugin install failures through the unified
`codex_plugin_install_failed` event for local installs, remote install
preflight failures, bundle failures, and remote catalog/backend
failures.
- Send classified `error_type` values in plugin install failure
analytics instead of raw error strings.
- Stop sending raw external-agent import errors in analytics while
preserving raw failure details in app-facing import
notifications/history.
- Keep raw plugin/migration diagnostics in `tracing::warn!` logs.
- Keep remote failure plugin names as the existing local placeholder
(`unknown`) and remove the extra telemetry plugin-name override.
- Change `ExternalAgentConfigImportParams.source` from a generated enum
to `string | null`, with legacy `claudeCode` / `claudeCowork` inputs
normalized to existing analytics values.
## Testing
## Summary
- Revert #28655, restoring the thread `recencyAt` behavior introduced by
#27910.
- Move `threads_recency_at` to migration 0039 so it no longer collides
with `external_agent_config_imports` at version 0038.
- Repair databases that already applied the recency migration as version
38 by moving the matching migration-history row to version 39 before
SQLx validation. The current version-38 migration can then apply
normally.
## Validation
- `just test -p codex-state
migrations::tests::repairs_recency_migration_that_was_applied_as_version_38`
- `just test -p codex-state -p codex-rollout -p codex-thread-store -p
codex-app-server-protocol -p codex-tui`: 3,439 passed; six TUI tests
could not open the machine's existing read-only incident database at
`~/.codex/sqlite/state_5.sqlite`.
- `just fix -p codex-state`
- `just fmt`
- Verified that state migration versions are unique.
## Why
Command approval cache keys included the command and working directory,
but not the execution environment. An approval for `/workspace` locally
could therefore be reused for the same command and path on an executor.
## What changed
- Include the selected environment ID in shell and unified-exec approval
cache keys.
- Carry that ID through the normal command approval request so clients
can show which environment is being approved.
- Expose the environment through app-server as a required nullable
`environmentId` and show it in the inline TUI approval prompt.
- Keep older recorded approval events compatible when the environment is
absent.
For example, `echo ok` in local `/workspace` and `echo ok` in executor
`/workspace` now produce different approval keys and separate prompts.
## Scope
This PR does not change network approvals, Guardian review actions, MCP
elicitation, full-screen TUI rendering, or environment-ID validation.
Remote `shell_command` execution itself remains in #28722; this PR only
makes its approval key environment-aware.
## Why
Revert #27910 to remove the newly introduced thread `recencyAt`
persistence and API behavior from `main`.
## What changed
This reverts commit `fac3158c2a783095768076489815f361fa9b0db4`,
including the state migration, thread-store propagation, app-server API
surface, generated schemas, and related tests.
## Validation
Not run before opening; relying on CI for the initial fast signal.
## Summary
Add a server-owned `recencyAt` timestamp and `recency_at` thread-list
sort key for product recency ordering while preserving the existing
meaning of `updatedAt` as the latest persisted thread mutation.
This is the server-side alternative to #27697. Rather than narrowing
`updatedAt`, clients can sort the sidebar by `recency_at` and continue
treating `updatedAt` as mutation time.
Paired Codex Apps PR:
[openai/openai#1024599](https://github.com/openai/openai/pull/1024599)
## Contract
- `recencyAt` initializes when a thread is created.
- A turn start advances `recencyAt` monotonically.
- Commentary, agent output, tool results, token/accounting updates, turn
completion, archive, unarchive, resume, and generic metadata writes do
not advance it.
- `updatedAt` retains its existing behavior and continues to advance for
persisted thread mutations.
- Current servers populate `recencyAt`; the response field is optional
in generated TypeScript so clients connected to older servers can fall
back to `updatedAt`.
- Filesystem-only fallback uses existing updated/mtime ordering when
SQLite is unavailable.
## Persistence and compatibility
Migration 0038 adds second- and millisecond-precision recency columns,
backfills them from the existing updated timestamp, creates list
indexes, and includes an insert trigger so older binaries writing to a
migrated database seed recency without causing later mutations to
advance it.
Generic metadata upserts preserve existing recency values. Turn-start
updates use a dedicated monotonic touch, and process-local allocation
keeps millisecond cursor values unique. State DB list, search, read,
filtered-list repair, rollout fallback propagation, and app-server
conversions all carry the new field.
## API
`Thread` responses include:
```ts
recencyAt?: number
```
`thread/list` and `thread/search` accept:
```json
{ "sortKey": "recency_at" }
```
Generated TypeScript and JSON schemas are included.
## Validation
- `just test -p codex-state` — 146 passed
- `just test -p codex-rollout` — 69 passed
- `just test -p codex-thread-store` — 81 passed
- `just test -p codex-app-server-protocol` — 231 passed
- Focused app-server list ordering, response mapping, archive/unarchive,
and resume lifecycle tests passed
- Scoped `just fix` for state, rollout, thread-store,
app-server-protocol, and app-server
- `just fmt`
- `git diff --check`
- Independent correctness, simplicity, elegance, security, and
test-quality reviews; actionable ordering, lifecycle, query-projection,
and timestamp-uniqueness findings were addressed
## Why
app-server may run on a different OS from the selected exec-server
environment. Parsing that environment’s cwd with the Codex host’s path
rules prevents thread startup.
## What
Carry environment cwd values as `LegacyAppPathString` at the app-server
boundary and `PathUri` internally. Existing tool-call schemas and
relative-path behavior stay host-native; remaining local-only consumers
convert explicitly and leave follow-up TODOs.
The Wine integration test verifies app-server can start a thread and
complete an ordinary turn with a Windows environment cwd from Linux.
## Validation
- `bazel test //codex-rs/core/tests/remote_env_windows:smoke-test
--test_output=errors`
- focused app-server environment-selection and protocol schema tests
- scoped Clippy for `codex-core` and `codex-app-server-protocol`
## Why
`ApiPathString` kind of implies that it can be used anywhere we pull a
path out of JSON, but it's not really appropriate for tool arguments
when the model might generate relative paths.
Prefer `String` for model-generated paths and we can handle the
conversion per feature for now and define a shared abstraction later if
it makes sense.
# What
Rename `ApiPathString` to `AppLegacyPathString` to clarify its role.
Expand the `path-types` skill to tell the model to leave tool args as
bare strings.
Add ThreadHistoryBuilder APIs for collecting incremental thread item and turn changes while applying rollout items.
Batch handling coalesces repeated changes so callers can get the latest incremental thread item changes for a set of rollout items without rebuilding full history.