Commit Graph

1849 Commits

  • config: remove legacy profile write paths (#24055)
    ## Why
    
    [#23883](https://github.com/openai/codex/pull/23883) moved the
    user-facing `--profile` flag onto profile v2 and
    [#23886](https://github.com/openai/codex/pull/23886) removed CLI
    forwarding for the legacy profile-v1 path. Core and TUI config
    persistence still carried `active_profile` and
    `ConfigEditsBuilder::with_profile`, which let later writes continue
    targeting legacy `[profiles.<name>]` tables after profile selection
    moved to profile-v2 config files.
    
    ## What
    
    - Remove legacy profile routing from
    [`ConfigEditsBuilder`](https://github.com/openai/codex/blob/4b38e9c22e762261d7f7eef49d8a21792e241a06/codex-rs/core/src/config/edit.rs#L1064-L1294),
    so core config edits no longer carry `with_profile` or infer
    `[profiles.*]` write targets from a `profile` key.
    - Drop `active_profile` plumbing from runtime `Config`, TUI
    startup/state, app-server config override forwarding, and Windows
    sandbox setup persistence.
    - Make app-server-backed TUI config edits use unscoped model,
    service-tier, feature, Auto-review, plan-mode, and Windows sandbox paths
    through
    [`tui/src/config_update.rs`](https://github.com/openai/codex/blob/4b38e9c22e762261d7f7eef49d8a21792e241a06/codex-rs/tui/src/config_update.rs#L43-L112).
    - Update config edit coverage so legacy `profile` state stays untouched
    by direct model writes, and remove tests whose only contract was the
    deleted profile-scoped persistence path.
    
    ## Testing
    
    - Not run locally.
  • config: remove legacy profile v1 resolution (#24051)
    ## Why
    
    [#23883](https://github.com/openai/codex/pull/23883) moved user-facing
    `--profile` selection onto profile v2, and
    [#23886](https://github.com/openai/codex/pull/23886) removed the old CLI
    `config_profile` override path. Core still had a second legacy path:
    `profile = "..."` could select `[profiles.*]` values while runtime
    config was built. Keeping that resolver alive preserves the old
    precedence model and profile-carrying surfaces even though profile
    selection now points at `$CODEX_HOME/<name>.config.toml`.
    
    ## What
    
    - Reject legacy top-level `profile = "..."` config while loading runtime
    config, with an error that points callers at `--profile <name>` and
    `<name>.config.toml` in the [core load
    path](https://github.com/openai/codex/blob/3d923366eca10a29143623124c6c6e538f058269/codex-rs/core/src/config/mod.rs#L2524-L2531).
    - Remove the remaining profile-v1 merge points from runtime config
    resolution, including features, permissions, model/provider selection,
    web search, Windows sandbox settings, TUI settings, role reloads, and
    OSS provider lookup.
    - Drop the leftover profile override surface from
    [`ConfigOverrides`](https://github.com/openai/codex/blob/3d923366eca10a29143623124c6c6e538f058269/codex-rs/core/src/config/mod.rs#L2118-L2148)
    and from the MCP server `codex` tool schema.
    - Prune profile-precedence tests that only exercised the removed
    resolver and replace them with rejection coverage for the legacy
    selector.
    
    ## Testing
    
    - Not run in this metadata pass.
    - Added
    [`legacy_profile_selection_is_rejected`](https://github.com/openai/codex/blob/3d923366eca10a29143623124c6c6e538f058269/codex-rs/core/src/config/config_tests.rs#L7942-L7965)
    coverage for the new runtime guard.
  • Fix auto-review permission profile override (#23956)
    ## Summary
    The auto-review runtime sync path was assigning a raw
    `PermissionProfile` into `runtime_permission_profile_override`, whose
    field now expects `RuntimePermissionProfileOverride`. That broke the TUI
    Bazel build.
    
    This changes the assignment to store
    `RuntimePermissionProfileOverride::from_config(&self.config)`, matching
    the other runtime override paths and preserving the active profile and
    network metadata with the permission profile.
  • [3 of 4] tui: route feature and memory toggles through app server (#22915)
    ## Why
    Experimental feature toggles and memory settings can update several
    related config values in one interaction. Keeping those writes local in
    a remote TUI session is especially dangerous because the UI can diverge
    from the app-server config while also leaving behind partially stale
    supporting keys.
    
    This is **[3 of 4]** in a stacked series that moves TUI-owned config
    mutations onto app-server APIs.
    
    ## What changed
    - Routed feature flag persistence through app-server batch writes,
    including the supporting reviewer and permission updates used by
    guardian approval.
    - Routed Windows sandbox mode persistence and legacy Windows feature
    cleanup through app-server writes.
    - Routed memory settings through app-server batch writes and updated the
    TUI tests to exercise the embedded app-server path.
    
    ## Config keys affected
    - `features.<feature_key>`
    - `profiles.<profile>.features.<feature_key>`
    - `approval_policy`
    - `sandbox_mode`
    - `approvals_reviewer`
    - `windows.sandbox`
    - `features.experimental_windows_sandbox`
    - `features.elevated_windows_sandbox`
    - `features.enable_experimental_windows_sandbox`
    - Profile-scoped Windows legacy feature variants under
    `profiles.<profile>.features.*`
    - `memories.use_memories`
    - `memories.generate_memories`
    - Profile-scoped memory variants under `profiles.<profile>.memories.*`
    
    ## Suggested manual validation
    - Connect the TUI to a remote app server, toggle guardian approval on
    and off, and confirm the remote config updates
    `features.guardian_approval`, reviewer state, approval policy, and
    sandbox mode coherently.
    - Toggle a default-false experimental feature at the root level, disable
    it again, and confirm the key clears instead of lingering as an
    unnecessary explicit `false`.
    - Change memory settings and confirm the remote config updates both
    memory keys while the running TUI reflects the new state.
    - On Windows, switch sandbox mode through the TUI and confirm
    `windows.sandbox` is updated while the legacy Windows feature keys are
    cleared.
    
    ## Stack
    1. [#22913](https://github.com/openai/codex/pull/22913) `[1 of 4]`
    primary settings writes
    2. [#22914](https://github.com/openai/codex/pull/22914) `[2 of 4]` app
    and skill enablement
    3. [#22915](https://github.com/openai/codex/pull/22915) `[3 of 4]`
    feature and memory toggles
    4. [#22916](https://github.com/openai/codex/pull/22916) `[4 of 4]`
    startup and onboarding bookkeeping
  • TUI: skip goal replace prompt for completed goals (#23792)
    ## Why
    Users reported that the replacement confirmation feels unnecessary when
    the current thread goal is already complete. In that state, `/goal
    <objective>` is starting fresh rather than interrupting active work.
    
    ## What changed
    `/goal <objective>` now skips the replace confirmation when the existing
    goal has `complete` status and uses the existing fresh replacement path.
    Goals that are active, paused, blocked, usage-limited, or budget-limited
    still require confirmation before being replaced.
  • Improve /goal error messages for ephemeral sessions (#23796)
    ## Why
    
    When a user runs `/goal` in a temporary session, the TUI can currently
    surface an internal app-server failure such as `thread/goal/get failed
    in TUI`. That message is technically true, but it does not explain the
    actual constraint: goals require a saved session because goal state is
    persisted with the thread.
    
    This is especially confusing when `codex doctor` reports the background
    app-server as running in ephemeral mode, since that wording is easy to
    conflate with ephemeral thread/session behavior.
    
    ## What changed
    
    - Added a TUI-side formatter for thread-goal RPC failures in
    `codex-rs/tui/src/app/thread_goal_actions.rs`.
    - Detects app-server/core errors that indicate goals are unsupported for
    an ephemeral thread/session.
    - Replaces the internal RPC failure with a user-facing explanation:
    
    ```text
    Goals need a saved session. This session is temporary.
    Run `codex` to start a saved session, or `codex resume` / `/resume` to reopen one.
    ```
    
    - Preserves the existing generic failure wording for non-ephemeral goal
    errors.
    
    ## Verification
    
    - `cargo test -p codex-tui thread_goal_error_message --lib`
    
    I also tried `cargo test -p codex-tui`; it built successfully but the
    test runner aborted in an unrelated side-thread stack overflow
    (`app::tests::discard_side_thread_removes_agent_navigation_entry`),
    which reproduced when run by itself.
  • tui: plumb permission profile selection (#23708)
    ## Why
    
    The named-profile `/permissions` picker needs a small TUI action path
    that can select permission profiles without folding the menu UI and
    profile metadata into the same review.
    
    ## What changed
    
    - Carry permission-profile selections through the TUI app event flow.
    - Persist selected profiles while preserving the existing approval
    settings and guardrail prompts.
    - Keep the legacy `/permissions` picker behavior in this layer; the
    profile-mode menu stays in the follow-up PR.
    
    ## Stack
    
    1. [#22931](https://github.com/openai/codex/pull/22931):
    runtime/session/network propagation for active permission profiles.
    2. **This PR**: TUI selection plumbing and guardrail flow.
    3. [#21559](https://github.com/openai/codex/pull/21559): profile-aware
    `/permissions` menu and custom profile display.
    
    <img width="1632" height="1186" alt="image"
    src="https://github.com/user-attachments/assets/69ddcd5e-b57c-468d-8c1d-246916323c15"
    />
    
    ## Validation
    
    - `git diff --cached --check` before commit.
    - Full test run skipped at the user request while pushing the split
    stack.
  • cli: remove legacy profile v1 plumbing (#23886)
    ## Why
    
    [#23883](https://github.com/openai/codex/pull/23883) moved the
    user-facing `--profile` flag onto profile v2. The shared CLI option
    layer still carried the old `config_profile` slot and several CLI
    entrypoints still copied that value into legacy config overrides.
    Leaving that path around makes the CLI surface look like it still
    selects legacy `[profiles.*]` state even though `--profile` now means
    `$CODEX_HOME/<name>.config.toml`.
    
    ## What
    
    - Remove the legacy `config_profile` field and merge/copy path from
    [`SharedCliOptions`](https://github.com/openai/codex/blob/95baaf72920c8db22097df8d15a0bb76c84528b6/codex-rs/utils/cli/src/shared_options.rs#L8-L177).
    - Stop forwarding profile-v1 overrides from CLI, exec, TUI, doctor,
    debug, feature, and exec-server paths; runtime profile selection remains
    on `config_profile_v2` through
    [`loader_overrides_for_profile`](https://github.com/openai/codex/blob/95baaf72920c8db22097df8d15a0bb76c84528b6/codex-rs/cli/src/main.rs#L1606-L1619).
    - Resolve local OSS provider selection from the base config in exec and
    TUI now that the legacy profile argument is gone.
    
    ## Testing
    
    - Not run (cleanup-only follow-up to #23883).
  • feat: support managed permission profiles in requirements.toml (#23433)
    ## Why
    
    Cloud-managed `requirements.toml` should be able to define the managed
    permission profiles a client may select and constrain that selectable
    set without requiring local user config to recreate the profile catalog.
    
    This keeps requirements focused on restrictions. The selected default
    remains a config or session choice, while requirements contribute the
    managed profile bodies and `allowed_permissions` allowlist that the
    config-loading boundary validates before a resolved runtime
    `PermissionProfile` is installed.
    
    ## What changed
    
    - Add `requirements.toml` support for a managed permission-profile
    catalog plus its allowlist:
    
    ```toml
    allowed_permissions = ["review", "build"]
    
    [permissions.review]
    extends = ":read-only"
    
    [permissions.build]
    extends = ":workspace"
    ```
    
    - Merge requirements-defined profile bodies into the effective
    permission catalog and reject profile ids that collide with
    config-defined profiles.
    - Validate that every `allowed_permissions` entry resolves to a built-in
    or catalog profile before selection uses it.
    - Preserve allowed configured named-profile selections. When a
    configured named profile is disallowed, fall back to the first allowed
    requirements profile with a startup warning.
    - Keep built-in selections and the stock trust-based `:read-only` /
    `:workspace` fallback path intact when no permission profile is
    explicitly selected.
    - Centralize the managed catalog and allowlist selection path in
    `EffectivePermissionSelection` so the requirements boundary is visible
    in config loading.
    - Surface `allowedPermissions` through `configRequirements/read`, and
    update the generated app-server schema fixtures plus the app-server
    README.
    
    ## Validation
    
    - `cargo test -p codex-config`
    - `cargo test -p codex-core system_requirements_`
    - `cargo test -p codex-core system_allowed_permissions_`
    - `cargo test -p codex-app-server-protocol`
    - `just write-app-server-schema`
    
    ## Related work
    
    - Uses merged permission-profile inheritance support from #22270 and
    #23705.
    - Kept separate from the in-flight permission profile listing API in
    #23412.
  • [codex] Add plugin id to MCP tool call items (#23737)
    Add owning plugin id to MCP tool call items so we can better filter them
    at plugin level.
    
    ## Summary
    - add optional `plugin_id` to MCP tool-call items and legacy begin/end
    events
    - propagate plugin metadata into emitted core items and app-server v2
    `ThreadItem::McpToolCall`
    - preserve plugin ids through app-server replay/redaction paths and
    regenerate v2 schema fixtures
    
    ## Testing
    - `just write-app-server-schema`
    - `just fmt`
    - `just fix -p codex-core`
    - `cargo test -p codex-protocol -p codex-app-server-protocol`
    - `cargo test -p codex-app-server-protocol`
    - `cargo test -p codex-core mcp_tool_call_item_includes_plugin_id --lib`
    - `cargo check -p codex-tui --tests`
    - `cargo check -p codex-app-server --tests`
    - `git diff --check`
    
    ## Notes
    - `just fix -p codex-core` completed with two non-fatal
    `too_many_arguments` warnings on the touched MCP notification helpers.
    - A broader `cargo test -p codex-core` run passed core unit tests, then
    hit shell/sandbox/snapshot failures in the integration target.
    - A broader app-server downstream run hit the existing
    `in_process::tests::in_process_start_clamps_zero_channel_capacity` stack
    overflow; `cargo test -p codex-exec` also hit the existing sandbox
    expectation mismatch in
    `thread_lifecycle_params_include_legacy_sandbox_when_no_active_profile`.
  • Honor client-resolved service tier defaults (#23537)
    ## Why
    
    Model catalog responses can now advertise a nullable
    `default_service_tier` for each model. Codex needs to preserve three
    distinct states all the way from config/app-server inputs to inference:
    
    - no explicit service tier, so the client may apply the current model
    catalog default when FastMode is enabled
    - explicit `default`, meaning the user intentionally wants standard
    routing
    - explicit catalog tier ids such as `priority`, `flex`, or future tiers
    
    Keeping those states distinct prevents the UI from showing one tier
    while core sends another, especially after model switches or app-server
    `thread/start` / `turn/start` updates.
    
    ## What Changed
    
    - Plumbed `default_service_tier` through model catalog protocol types,
    app-server model responses, generated schemas, model cache fixtures, and
    provider/model-manager conversions.
    - Added the request-only `default` service tier sentinel and normalized
    legacy config spelling so `fast` in `config.toml` still materializes as
    the runtime/request id `priority`.
    - Moved catalog default resolution to the TUI/client side, including
    recomputing the effective service tier when model/FastMode-dependent
    surfaces change.
    - Updated app-server thread lifecycle config construction so
    `serviceTier: null` preserves explicit standard-routing intent by
    mapping to `default` instead of internal `None`.
    - Kept core responsible for validating explicit tiers against the
    current model and stripping `default` before `/v1/responses`, without
    applying catalog defaults itself.
    
    ## Validation
    
    - `CARGO_INCREMENTAL=0 cargo build -p codex-cli`
    - `CARGO_INCREMENTAL=0 cargo test -p codex-app-server model_list`
    - `cargo test -p codex-tui service_tier`
    - `cargo test -p codex-protocol service_tier_for_request`
    - `cargo test -p codex-core get_service_tier`
    - `RUST_MIN_STACK=8388608 CARGO_INCREMENTAL=0 cargo test -p codex-core
    service_tier`
  • Add SubagentStop hook (#22873)
    # What
    
    <img width="1792" height="1024" alt="image"
    src="https://github.com/user-attachments/assets/8f81d232-5813-4994-a61d-e42a05a93a3e"
    />
    
    `SubagentStop` runs when a thread-spawned subagent turn is about to
    finish. Thread-spawned subagents use `SubagentStop` instead of the
    normal root-agent `Stop` hook.
    
    Configured handlers match on `agent_type`. Hook input includes the
    normal stop fields plus:
    
    - `agent_id`: the child thread id.
    - `agent_type`: the resolved subagent type.
    - `agent_transcript_path`: the child subagent transcript path.
    - `transcript_path`: the parent thread transcript path.
    - `last_assistant_message`: the final assistant message from the child
    turn, when available.
    - `stop_hook_active`: `true` when the child is already continuing
    because an earlier stop-like hook blocked completion.
    
    `SubagentStop` shares the same completion-control semantics as `Stop`,
    scoped to the child turn:
    
    - No decision allows the child turn to finish.
    - `decision: "block"` with a non-empty `reason` records that reason as
    hook feedback and continues the child with that prompt.
    - `continue: false` stops the child turn. If `stopReason` is present,
    Codex surfaces it as the stop reason.
    
    # Lifecycle Scope
    
    Only thread-spawned subagents run `SubagentStop`.
    
    Internal/system subagents such as Review, Compact, MemoryConsolidation,
    and Other do not run normal `Stop` hooks and do not run `SubagentStop`.
    This avoids exposing synthetic matcher labels for internal
    implementation paths.
    
    # Stack
    
    1. #22782: add `SubagentStart`.
    2. This PR: add `SubagentStop`.
    3. #22882: add subagent identity to normal hook inputs.
  • core: refresh active permission profiles at runtime (#22931)
    ## Why
    
    Once a named permission profile is selected, runtime state has to keep
    that profile identity intact instead of collapsing back to anonymous
    effective permissions. The session refresh path also needs to rebuild
    profile-derived network proxy state so active profile switches take
    effect consistently.
    
    ## What changed
    
    - Preserve the active permission profile through session updates.
    - Rebuild profile-derived runtime/network configuration when the active
    profile changes.
    - Keep the runtime path aligned with the current session configuration
    APIs.
    - Tighten the affected tests, including the Windows delete-pending
    memory-file case that was intermittently tripping CI.
    
    ## Stack
    
    1. **This PR**: runtime/session/network propagation for active
    permission profiles.
    2. [#23708](https://github.com/openai/codex/pull/23708): TUI selection
    plumbing and guardrail flow.
    3. [#21559](https://github.com/openai/codex/pull/21559): profile-aware
    `/permissions` menu and custom profile display.
    
    <img width="1296" height="906" alt="image"
    src="https://github.com/user-attachments/assets/077fa3a7-80cb-4925-80b1-d2395018d90a"
    />
  • Fix thread settings clippy failure (#23724)
    ## Why
    
    `main` picked up two small Rust build failures after nearby merges:
    
    - #23507 added a real handler for
    `ServerNotification::ThreadSettingsUpdated`, but the same variant was
    still listed in the ignored-notification match arm. Full Clippy runs
    treat the resulting unreachable-pattern warning as an error.
    - #23666 added `turn_id` and `truncation_policy` to
    `codex_tools::ToolCall`, while the goal extension backend test fixtures
    from the goal-extension work still used the old shape. That left
    `codex-goal-extension` tests unable to compile once the branches met on
    `main`.
    
    ## What changed
    
    Removed the duplicate `ThreadSettingsUpdated` match pattern from
    `tui/src/chatwidget/protocol.rs`.
    
    Updated the goal extension test `tool_call` helper to populate the new
    `ToolCall` fields, and reused that helper for the one direct literal
    that still had the old field list.
    
    ## Verification
    
    - `just fix -p codex-tui`
    - `cargo test -p codex-goal-extension`
  • Sync TUI thread settings through app server (#23507)
    Builds on #23502.
    
    ## Why
    
    #23502 adds the app-server `thread/settings/update` API and matching
    `thread/settings/updated` notification. The TUI already lets users
    change thread-scoped settings such as model, reasoning effort, service
    tier, approvals, permissions, personality, and collaboration mode, but
    those updates need to flow through the app server so embedded and
    connected clients observe the same thread state.
    
    This is a rework (simplification) of PR
    https://github.com/openai/codex/pull/22510. It has the same
    functionality, but the underlying `thread/settings/update` api is now
    simpler in that it no longer returns the effective settings as a
    response. Now, clients receive the effective settings only through the
    `thread/settings/updated` notification.
    
    ## What Changed
    
    This updates the TUI to send `thread/settings/update` whenever those
    thread-scoped settings change and to treat the RPC response as the
    authoritative acknowledgement. It also routes `thread/settings/updated`
    notifications back into cached session state and the visible chat widget
    so active and inactive threads stay in sync after app-server-originated
    changes.
    
    The implementation is kept to the TUI layer: settings conversion and
    merge logic live under `codex-rs/tui/src/app/thread_settings.rs`, with
    dispatch/routing hooks in the existing app and chat widget paths.
    
    ## Verification
    
    I manually tested using `codex app-server --listen unix://` and then
    launching two copies of the TUI that use the same local app server. I
    then resumed the same thread on both and verified that changes like plan
    mode, fast mode, model, reasoning effort, etc. are reflected "live" in
    the second client when modified in the first and vice versa.
  • Add thread/settings/update app-server API (#23502)
    ## Why
    
    App-server clients need a way to update a thread's next-turn settings
    without starting a turn, adding transcript content, or waiting for turn
    lifecycle events. This gives settings UI a direct path for durable
    thread settings while clients observe the eventual effective state
    through a notification.
    
    This is a simplified rework of PR
    https://github.com/openai/codex/pull/22509. In particular, it changes
    the `thread/settings/update` api to return immediately rather than
    waiting and returning the effective (updated) thread settings. This
    makes the new api consistent with `turn/start` and greatly reduces the
    complexity of the implementation relative to the earlier attempt.
    
    ## What Changed
    
    - Adds experimental `thread/settings/update` with partial-update request
    fields and an empty acknowledgment response.
    - Adds experimental `thread/settings/updated`, carrying full effective
    `ThreadSettings` and scoped by `threadId` to subscribed clients for the
    affected thread.
    - Shares durable settings validation with `turn/start`, including
    `sandboxPolicy` plus `permissions` rejection and `serviceTier: null`
    clearing.
    - Emits the same settings notification when `turn/start` overrides
    change the stored effective thread settings.
    - Regenerates app-server protocol schema fixtures and updates
    `app-server/README.md`.
  • [2 of 2] Start fresh TUI thread in background (#23176)
    ## Why
    
    After the terminal-probe work in #23175, fresh-session startup still
    waits for `thread/start` before the chat input can become usable. The
    chat widget already has the machinery to hold early submissions until a
    session is configured, so fresh `thread/start` does not need to stay on
    the input-ready hot path.
    
    Refs #16335.
    
    ## What
    
    This PR starts fresh app-server threads in a background task, reports
    completion through a startup app event, and attaches the primary session
    once `thread/start` returns. Resume and fork startup paths remain
    synchronous.
    
    ## Benchmark
    
    In the local pty startup benchmark, this PR's pre-optimization base
    branch, #23175, measured about 152ms median from launch to accepted chat
    input. The stacked result measured about 66ms median, for an approximate
    additional savings of 85-95ms. For broader context, the original `main`
    baseline before either startup optimization was about 250.5ms median. We
    also measured Codex 0.117.0 on the same machine at about 64.6ms median,
    so the stacked branch is back in the old-startup-time range.
    
    ## Stack
    
    1. [#23175: [1 of 2] Optimize TUI startup terminal
    probes](https://github.com/openai/codex/pull/23175) — base PR
    2. [#23176: [2 of 2] Start fresh TUI thread in
    background](https://github.com/openai/codex/pull/23176) — this PR
    
    ## Verification
    
    - `cargo test -p codex-tui`
  • runtime: detect Codex package layout (#23596)
    ## Why
    
    The package-builder stack now creates a canonical Codex package
    directory where the entrypoint lives under `bin/`, bundled helper
    resources live under `codex-resources/`, and bundled PATH-style tools
    live under `codex-path/`. That layout is not specific to the standalone
    installer: npm, brew, install scripts, and manually unpacked artifacts
    should all be able to use the same package shape.
    
    The Rust runtime still only knew about the legacy standalone release
    layout, where resources sit next to the executable. A packaged binary
    therefore would not identify its package root or prefer the bundled `rg`
    from `codex-path/`.
    
    ## What changed
    
    - Adds `CodexPackageLayout` to `codex-install-context` and detects it
    from an executable path shaped like `<package>/bin/<entrypoint>` when
    `<package>/codex-package.json` is present.
    - Splits `InstallContext` into an install `method` plus an optional
    package layout so the layout is shared across npm, bun, brew,
    standalone, and other launch contexts.
    - Stores package-layout paths as `AbsolutePathBuf` values.
    - Keeps `codex-resources/` and `codex-path/` optional so Codex can still
    run with degraded behavior if sidecar directories are missing.
    - Updates `InstallContext::rg_command()` to prefer bundled
    `codex-path/rg` or `rg.exe`, then fall back to the legacy standalone
    resources location, then system `rg`.
    - Updates `codex doctor` reporting so package installs show package,
    bin, resources, and path directories, and so bundled search detection
    recognizes `codex-path/` for any install method.
    
    ## Test plan
    
    - `cargo test -p codex-install-context`
    - `cargo test -p codex-cli`
    - `cargo test -p codex-tui
    update_action::tests::maps_install_context_to_update_action`
    - `just bazel-lock-check`
  • Fix stale background terminal poll events (#23231)
    ## Why
    
    Issue #23214 reports `/ps` showing no background terminals while the
    status line still says it is waiting for a background terminal. The race
    is in core: `write_stdin` can poll a process that exits before the
    response returns. The process manager correctly returns `process_id:
    None`, but the handler still emitted a `TerminalInteraction` event using
    the requested session id, causing clients to believe a dead process was
    still being polled.
    
    Fixes #23214.
    
    ## What changed
    
    - Suppress `TerminalInteraction` events for empty `write_stdin` polls
    once `response.process_id` is `None`.
    - Continue emitting interactions for non-empty stdin, even if that input
    causes the process to exit before the response returns.
    - Extend the unified exec integration test to assert completed empty
    polls do not emit terminal interactions.
    
    ## Verification
    
    - `cargo test -p codex-core --test all
    unified_exec_emits_one_begin_and_one_end_event`
    - `cargo test -p codex-core --test all
    unified_exec_emits_terminal_interaction_for_write_stdin`
    
    `cargo test -p codex-core` currently aborts in unrelated
    `agent::control::tests::resume_agent_from_rollout_uses_edge_data_when_descendant_metadata_source_is_stale`
    with a reproducible stack overflow.
  • Fix: TUI starting in wrong CWD (#23538)
    This fixes a regression wher codex could start in the wrong directory
    when a live local app-server socket was present. The issue was that
    implicit local socket reuse was being treated like an explicit remote
    workspace session, which dropped the invoking cwd unless --cd was
    passed.
    
    The change separates local socket transport from true remote workspace
    semantics.
    - Plain local startup keeps local cwd, trust, resume, picker, and
    config-refresh behavior.
    - Explicit --remote keeps the existing remote cwd behavior.
    - Added coverage for launch target selection and local-session
    filtering/cwd behavior.
    
    Steps to test:
    - Start a local app-server from a different directory than the repo you
    want to use.
      - Launch codex from a project/worktree without --cd.
    - Confirm the session starts in the invoking directory, not the
    app-server process directory.
    - Confirm explicit codex --remote ... still preserves existing remote
    behavior.
  • Add CUA requirements subsection for locked computer use (#23555)
    Adds a new top-level section for "CUA" requirements that can allow for
    disablement of specific features as needed for enterprises.
  • fix(tui): preserve modified enter in plan questions (#23536)
    ## Why
    
    Plan mode questionnaires reuse the shared composer for free-form
    answers, but the surrounding `request_user_input` overlay still treated
    every `KeyCode::Enter` as “advance to the next question.” That made
    `Shift+Enter` insert a newline in the composer and then immediately
    advance the questionnaire anyway.
    
    Fixes #23448.
    
    ## What Changed
    
    - pass the live `RuntimeKeymap` into `RequestUserInputOverlay` so its
    embedded composer honors existing `/keymap` composer/editor remaps
    - advance free-form questions only on the configured composer submit
    binding, instead of any Enter-shaped key event
    - add regressions for `Shift+Enter` newline behavior and configured
    composer submit bindings inside the questionnaire UI
    
    ## How to Test
    
    1. Start Codex in Plan mode and trigger a `request_user_input`
    questionnaire with a free-form answer field.
    2. Focus the free-form field, type a line, then press `Shift+Enter`.
    3. Confirm the answer gains a newline and the questionnaire stays on the
    same question.
    4. Press the configured submit binding, or plain `Enter` with the
    default keymap, and confirm the questionnaire advances as before.
    
    Targeted tests:
    - `cargo test -p codex-tui
    bottom_pane::request_user_input::tests::freeform_ -- --nocapture`
    
    ## Notes
    
    - `cargo test -p codex-tui` still reaches an unrelated existing stack
    overflow in
    `app::tests::discard_side_thread_removes_agent_navigation_entry` on this
    checkout.
    - `just argument-comment-lint` is locally blocked by Bazel analysis
    failing in external `compiler-rt` before the lint runs.
  • Make local environment optional in EnvironmentManager (#23369)
    ## Summary
    - make `EnvironmentManager` local environment/runtime paths optional
    - simplify constructor surface around snapshot materialization
    - rename local env accessors to `require_local_environment` /
    `try_local_environment`
    
    ## Validation
    - devbox Bazel build for touched crate surfaces
    - `//codex-rs/exec-server:exec-server-unit-tests`
    - `//codex-rs/app-server-client:app-server-client-unit-tests`
    - filtered touched `//codex-rs/core:core-unit-tests` cases
  • Add SubagentStart hook (#22782)
    # What
    
    `SubagentStart` runs once when Codex creates a thread-spawned subagent,
    before that child sends its first model request. Thread-spawned
    subagents use `SubagentStart` instead of the normal root-agent
    `SessionStart` hook.
    
    Configured handlers match on the subagent `agent_type`, using the same
    value passed to `spawn_agent`. When no agent type is specified, Codex
    uses the default agent type.
    
    Hook input includes the normal session-start fields plus:
    
    - `agent_id`: the child thread id.
    - `agent_type`: the resolved subagent type.
    
    `SubagentStart` may return `hookSpecificOutput.additionalContext`. That
    context is added to the child conversation before the first model
    request.
    
    # Lifecycle Scope
    
    Only thread-spawned subagents run `SubagentStart`.
    
    Internal/system subagents such as Review, Compact, MemoryConsolidation,
    and Other do not run normal `SessionStart` hooks and do not run
    `SubagentStart`. This avoids exposing synthetic matcher labels for
    internal implementation paths.
    
    Also the `SessionStart` hook no longer fires for subagents, this matches
    behavior with other coding agents' implementation
    
    # Stack
    
    1. This PR: add `SubagentStart`.
    2. #22873: add `SubagentStop`.
    3. #22882: add subagent identity to normal hook inputs.
  • Harden CLI rate limit window labels (#22929)
    ## Context
    
    The CLI rate-limit surfaces previously described usage windows as fixed
    5-hour and weekly limits. We want the CLI to display whatever supported
    rate-limit period the server returns instead of assuming a 5-hour/1-week
    pair. This supports generalized Codex rate-limit periods.
    
    ## Summary
    
    - Formats CLI rate-limit warning/status labels only for the supported
    returned window durations: approximate 5h, daily, weekly, monthly, and
    annual.
    - Uses generic fallback copy when a primary or secondary window has no
    duration, so missing secondary protection data does not produce stale
    weekly copy.
    - Uses generic fallback copy for unsupported window durations instead of
    adding arbitrary hourly, multi-day, multi-week, or multi-year labels.
    - Updates status line and terminal title setup descriptions/previews to
    talk about primary/secondary usage limits rather than fixed 5h/weekly
    limits.
    - Adds rendered insta snapshot coverage for the updated rate-limit
    status surfaces and `/status` fallback labels.
    
    ## Tests
    Tested locally:
    - one primary window
    - one secondary window
    - primary and secondary window
  • Make deny canonical for filesystem permission entries (#23493)
    ## Why
    Filesystem permission profiles used `none` for deny-read entries, which
    is less direct than the action the entry actually represents. This
    change makes `deny` the canonical filesystem permission spelling while
    preserving compatibility for older configs that still send `none`.
    
    ## What changed
    - rename `FileSystemAccessMode::None` to `Deny`
    - serialize and generate schemas with `deny` as the canonical value
    - retain `none` only as a legacy input alias for temporary config
    compatibility
    - update filesystem glob diagnostics and regression coverage to use the
    canonical spelling
    - refresh config and app-server schema fixtures to match the new wire
    shape
    
    ## Validation
    - `cargo test -p codex-protocol`
    - `cargo test -p codex-app-server-protocol`
    - `cargo test -p codex-core config_toml_deserializes_permission_profiles
    --lib`
    - `cargo test -p codex-core
    read_write_glob_patterns_still_reject_non_subpath_globs --lib`
    
    Earlier in the session, a broad `cargo test -p codex-core` run reached
    unrelated pre-existing failures in timing/snapshot/git-info tests under
    this environment; the targeted surfaces touched by this PR passed
    cleanly.
  • [2 of 4] tui: route app and skill enablement through app server (#22914)
    ## Why
    App and skill toggles are user config mutations too. When the TUI is
    attached to a remote app server, writing those toggles into the local
    `config.toml` makes the UI report success without updating the server
    that actually owns the session.
    
    This is **[2 of 4]** in a stacked series that moves TUI-owned config
    mutations onto app-server APIs.
    
    ## What changed
    - Routed app enable/disable persistence through app-server config batch
    writes.
    - Routed skill enable/disable persistence through `skills/config/write`.
    - Avoided refreshing local config from disk after these writes when the
    TUI is connected to a remote app server.
    
    ## Config keys affected
    - `apps.<app_id>.enabled`
    - `apps.<app_id>.disabled_reason`
    - `[[skills.config]]` entries keyed by `path`, with `enabled = false`
    used for persisted disables
    
    ## Suggested manual validation
    - Connect the TUI to a remote app server, disable an app, reconnect, and
    confirm the app remains disabled from remote config rather than local
    disk state.
    - Re-enable the same app and confirm both `apps.<app_id>.enabled` and
    `apps.<app_id>.disabled_reason` are cleared remotely.
    - Disable a skill in the manage-skills UI and confirm a remote
    `[[skills.config]]` disable entry appears.
    - Re-enable that skill and confirm the disable entry is removed and the
    effective enabled state updates without relying on local config reloads.
    
    ## Stack
    1. [#22913](https://github.com/openai/codex/pull/22913) `[1 of 4]`
    primary settings writes
    2. [#22914](https://github.com/openai/codex/pull/22914) `[2 of 4]` app
    and skill enablement
    3. [#22915](https://github.com/openai/codex/pull/22915) `[3 of 4]`
    feature and memory toggles
    4. [#22916](https://github.com/openai/codex/pull/22916) `[4 of 4]`
    startup and onboarding bookkeeping
  • app-server: use profile ids in v2 permission params (#23360)
    ## Why
    
    The v2 app-server permission profile fields are experimental, but the
    previous migration kept a legacy object payload for profile selection.
    That made clients aware of server-owned `activePermissionProfile`
    metadata such as `extends`, and it kept a
    `legacy_additional_writable_roots` path even though
    `runtimeWorkspaceRoots` now owns runtime workspace-root selection.
    
    This PR makes the client contract match the intended model: clients
    select a permission profile by id, and the server resolves and reports
    active profile provenance in response payloads.
    
    Follow-up to #22611.
    
    ## What Changed
    
    - Changed `thread/start`, `thread/resume`, `thread/fork`, and
    `turn/start` permission profile selection to plain profile id strings.
    - Changed `command/exec.permissionProfile` to a plain profile id string
    for the same client/server ownership split.
    - Removed `PermissionProfileSelectionParams` and the legacy `{ type:
    "profile", modifications: [...] }` compatibility deserializer.
    - Updated app-server, TUI, and `codex exec` call sites to send only ids,
    while keeping `activePermissionProfile` as server response metadata.
    - Updated app-server docs and schema fixtures for the revised
    `command/exec.permissionProfile` shape.
    
    ## Verification
    
    - `cargo test -p codex-app-server-protocol`
    - `RUST_MIN_STACK=8388608 cargo test -p codex-app-server`
    - `cargo test -p codex-exec`
    - `RUST_MIN_STACK=8388608 cargo test -p codex-tui`
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/23360).
    * #23368
    * __->__ #23360
  • fix(tui): warn on unsupported iTerm2 pet versions (#23371)
    ## Why
    
    Older iTerm2 builds can be detected as supporting the image transport
    that terminal pets use, but in practice they fail to render the pet flow
    correctly. Instead of silently attempting image rendering, Codex should
    tell the user that their iTerm2 version is too old and that upgrading is
    the fix.
    
    ## What Changed
    
    - gate iTerm2 pet auto-detection on version `3.6.0` or newer
    - show a dedicated upgrade message for older or unknown iTerm2 versions
    instead of the generic unsupported-terminal warning
    - keep the existing generic unsupported-terminal path for non-iTerm
    terminals
    - add regression coverage for iTerm2 version parsing and the old-iTerm
    warning path
    
    ## How to Test
    
    1. Start Codex in iTerm2 3.6 or newer.
    2. Run `/pets`.
    3. Confirm the pets picker opens instead of showing a warning.
    4. Start Codex in an older iTerm2 build, or exercise the equivalent test
    path.
    5. Run `/pets`.
    6. Confirm Codex warns that pets require iTerm2 3.6 or newer and tells
    the user to upgrade.
    7. Also verify that a non-iTerm unsupported terminal still shows the
    generic unsupported-terminal message.
    
    Targeted tests:
    - `cargo test -p codex-terminal-detection`
    - `cargo test -p codex-tui pets::`
    - `cargo test -p codex-tui slash_pets_on_unsupported_terminal`
    - `cargo test -p codex-tui slash_pets_on_old_iterm2`
  • feat(tui): handle paste in session picker (#23338)
    ## Why
    
    The session picker already supports typed search, but it ignored
    bracketed paste events entirely. On macOS terminals this makes pasted
    text look like a no-op on the resume screen, which is especially
    noticeable when a user wants to paste part of a thread name, branch, or
    path into the search field.
    
    ## What Changed
    
    - route `TuiEvent::Paste(String)` into the session picker instead of
    dropping it
    - normalize pasted search text into a single-line query by collapsing
    whitespace
    - ignore whitespace-only pastes
    - reuse the existing `set_query(...)` path so pasted searches keep the
    same filtering and pagination behavior as typed input
    - add focused tests for append behavior, whitespace normalization,
    whitespace-only paste, and the existing search-loading path
    
    This PR is stacked on top of #23234 and contains only the net change
    relative to `etraut/clarify-resume-hints`.
    
    ## How to Test
    
    1. Start Codex in a terminal that emits bracketed paste, for example
    iTerm2 on macOS.
    2. Open the resume picker so the search UI is visible.
    3. Copy a term that should match one of the visible sessions, then paste
    it into the picker.
    4. Confirm the query updates immediately and the list filters as if the
    text had been typed.
    5. Also verify that pasting text with newlines or tabs still produces a
    usable single-line search query.
    6. Also verify that normal typed search still works and that `Esc` still
    clears the query / exits as before.
    
    Targeted tests:
    - `cargo test -p codex-tui`
    
    ---------
    
    Co-authored-by: Eric Traut <etraut@openai.com>
  • TUI: replay in-progress MCP calls as started (#23236)
    Fixes #22300.
    
    ## Summary
    MCP tool calls can appear in thread history while still in progress.
    During replay, `handle_thread_item` routed every
    `ThreadItem::McpToolCall` to the completion handler, so an in-progress
    item with no result or error was rendered as `MCP tool call completed
    without a result`.
    
    This updates replay handling to mirror command executions: `InProgress`
    MCP calls go through `on_mcp_tool_call_started`, while completed and
    failed calls continue through the completion path.
    
    ## Validation
    - `cargo test -p codex-tui
    replayed_in_progress_mcp_tool_call_stays_active`
  • TUI: route elicitation responses to request thread (#23241)
    ## Why
    
    Fixes #21894.
    
    When the TUI handles an MCP elicitation, the request payload already
    includes the thread that generated the elicitation.
    `ChatWidget::handle_elicitation_request_now` was ignoring that value and
    using the currently visible chat thread instead. In a multi-session TUI,
    that can send `resolve_elicitation` to an older visible thread rather
    than the session that owns the pending elicitation, producing
    `elicitation request not found` and leaving the prompt unresolved.
    
    ## What changed
    
    - Parse `McpServerElicitationRequestParams.thread_id` in the ChatWidget
    elicitation handler and use it for app-link, form, fallback approval,
    and auto-decline resolution paths.
    - Keep the existing visible-thread fallback only for malformed request
    payloads with an invalid thread id.
    - Update the invalid URL elicitation regression test so the visible
    thread and request thread intentionally differ.
  • Clarify resume hints for renamed threads (#23234)
    Addresses #23181
    
    ## Why
    Renamed threads can share names, so hints that suggest resuming directly
    by name are ambiguous. Issue #23181 asks for the picker hint to include
    the thread name and thread ID in parens so users can disambiguate
    safely.
    
    ## What
    - Adds a shared resume hint formatter for named threads: run `codex
    resume`, then select `<name> (<thread-id>)`.
    - Uses that hint for /rename confirmations, TUI session summaries, and
    CLI/TUI exit messages.
    - Keeps direct `codex resume <thread-id>` guidance for unnamed threads.
    
    ## Verification
    Manually verified that message after `/rename` and after `/exit` include
    session ID in parens.
    
    ---------
    
    Co-authored-by: Felipe Coury <felipe.coury@openai.com>
  • goal: pause continuation loops on usage limits and blockers (#23094)
    Addresses #22833, #22245, #23067
    
    ## Why
    `/goal` can keep synthesizing turns even when the next turn cannot make
    meaningful progress. Hard usage exhaustion can replay failing turns, and
    repeated permission or external-resource blockers can keep burning
    tokens while waiting for user or system intervention.
    
    ## What changed
    - Add resumable `blocked` and `usageLimited` goal states. As with
    `paused`, goal continuation stops with these states.
    - Move to `usageLimited` after usage-limit failures.
    - Allow the built-in `update_goal` tool to set `blocked` only under
    explicit repeated-impasse guidance. Updated goal continuation prompt to
    specify that agent should use `blocked` only when it has made at least
    three attempts to get past an impasse.
    
    Most of the files touched by this PR are because of the small app server
    protocol update.
    
    ## Validation
    
    I manually reproduced a number of situations where an agent can run into
    a true impasse and verified that it properly enters `blocked` state. I
    then resumed and verified that it once again entered `blocked` state
    several turns later if the impasse still exists.
    
    I also manually reproduced the usage-limit condition by creating a
    simulated responses API endpoint that returns 429 errors with the
    appropriate error message. Verified that the goal runtime properly moves
    the goal into `usageLimited` state and TUI UI updates appropriately.
    Verified that `/goal resume` resumes (and immediately goes back into
    `ussageLImited` state if appropriate).
    
    
    ## Follow-up PRs
    
    Small changes will be needed to the GUI clients to properly handle the
    two new states.
  • fix(tui): show shutdown feedback on exit (#23323)
    ## Why
    
    Ctrl+C can take a noticeable amount of time to finish when the TUI is
    waiting for the app-server thread shutdown path to complete. Before this
    change, the UI could look like it had not accepted the shutdown request
    because the composer and cursor remained in their normal interactive
    state during that wait.
    
    This PR makes the accepted shutdown visible immediately. It does not add
    an artificial sleep or change the shutdown timeout; it only draws one
    final feedback frame before continuing through the existing shutdown
    flow.
    
    ## What Changed
    
    - On `ExitMode::ShutdownFirst`, the TUI now renders shutdown feedback
    before awaiting the existing thread shutdown future.
    - The bottom pane disables composer input, which hides the cursor
    through the existing disabled-input cursor path.
    - The composer shows `Shutting down...` as the disabled input hint and
    suppresses footer content so the shutdown acknowledgement is not
    competing with shortcut/status text.
    - The logout path uses the same feedback path before shutting down.
    
    ## How to Test
    
    1. Start Codex from this branch.
    2. Press `Ctrl+C` to request shutdown.
    3. If shutdown takes long enough to observe, confirm the composer
    changes to `› Shutting down...`, the cursor disappears, and no footer
    hint is rendered below it.
    4. Regression check: repeat with text already typed in the composer and
    confirm the visible row still switches to `Shutting down...` while the
    draft remains preserved internally until the process exits.
    
    Targeted tests:
    
    - `cargo test -p codex-tui
    shutdown_in_progress_disables_input_and_uses_hint_without_footer`
    - `cargo test -p codex-tui bottom_pane::footer::tests::`
    
    ## Local Validation Note
    
    `cargo test -p codex-tui` still aborts in
    `app::tests::discard_side_thread_removes_agent_navigation_entry` with a
    stack overflow. That same test also failed when run alone locally, and
    the failure appears unrelated to this shutdown feedback path.
  • [1 of 2] Optimize TUI startup terminal probes (#23175)
    ## Why
    
    Codex TUI startup still feels slower than 0.117.0 after the app-server
    move in 0.118.0. A visible chunk of launch-to-input latency comes from
    serial terminal startup probes: cursor position, keyboard enhancement
    support, and default foreground/background color queries can each wait
    on terminal responses before the first usable frame.
    
    Refs #16335.
    
    ## What
    
    This PR batches the terminal startup probes into one bounded probe. It
    also reuses the probed cursor position and default colors during TUI
    setup, fast-paths the primary-device-attributes fallback as keyboard
    enhancement unsupported, and keeps lightweight startup timing logs for
    future tuning.
    
    The startup telemetry is intentionally left in production: it records
    phase timings for terminal probes and initial-frame scheduling so future
    startup regressions can be diagnosed from normal logs rather than
    re-adding one-off debug instrumentation.
    
    ## Benchmark
    
    In the local pty startup benchmark, the pre-optimization `main` baseline
    was about 250.5ms median from launch to accepted chat input. This
    probe-only branch measured about 152ms median, for an approximate
    savings of 95-100ms.
    
    ## Stack
    
    1. [#23175: [1 of 2] Optimize TUI startup terminal
    probes](https://github.com/openai/codex/pull/23175) — this PR
    2. [#23176: [2 of 2] Start fresh TUI thread in
    background](https://github.com/openai/codex/pull/23176) — layered on
    this PR
    
    ## Verification
    
    - `cargo test -p codex-tui`
  • Hide ChatGPT usage link for non-OpenAI status (#23127)
    Addresses #22778
    
    ## Summary
    
    Provider deployments such as Bedrock manage rate limits and billing
    outside ChatGPT, so the `/status` link to the ChatGPT usage page is
    irrelevant and confusing for those users. Custom providers that are
    explicitly configured to use OpenAI/ChatGPT auth still point at
    OpenAI-backed usage, so they should keep the link.
    
    ## Changes
    
    - Render the ChatGPT usage note only when the configured provider uses
    OpenAI auth.
    - Keep the note hidden when `/status` displays a provider such as
    Bedrock that manages limits elsewhere.
    - Add regression coverage for both Bedrock and a custom OpenAI-auth
    proxy provider.
    
    ## Manual Repro
    
    1. Configure Codex with a non-OpenAI-auth provider, for example
    `model_provider = "amazon-bedrock"`.
    2. Start the TUI and run `/status`.
    3. Confirm the status card shows the custom provider, for example `Model
    provider: Amazon Bedrock`, and does not show
    `https://chatgpt.com/codex/settings/usage`.
    4. Configure a custom provider that proxies to OpenAI and has
    OpenAI/ChatGPT auth enabled.
    5. Run `/status` again and confirm the ChatGPT usage link appears for
    that OpenAI-auth provider.
  • Fix TUI stream cleanup after turn errors (#23128)
    ## Summary
    
    Fixes #22726.
    
    After a Responses stream disconnect, the live TUI could keep accepting
    prompts while leaving partially streamed assistant output in its
    transient streaming-cell form. That made fenced diffs or SVG/XML-like
    content appear as raw transcript text until the user closed the TUI and
    resumed the same session, which rebuilt the transcript from saved
    history.
    
    This change finalizes the active answer stream before generic
    failed-turn cleanup clears the stream controller, so the live transcript
    takes the same source-backed markdown consolidation path as a successful
    turn.
    
    ## Reviewer repro
    
    1. Start a local Codex TUI session.
    2. Trigger an assistant turn that streams markdown content, especially a
    fenced diff or SVG/XML-like block.
    3. Force or encounter a non-retry stream disconnect before the turn
    completes.
    4. Continue using the same still-open TUI session.
    5. Before this fix, the live history can stay raw/plain even though
    `codex resume` renders the same session normally.
    6. After this fix, the failed-turn path consolidates the partial stream
    before rendering the error, so the live TUI keeps normal transcript
    rendering.
  • tui: keep cleared Fast tier from reappearing after side-thread resume (#23121)
    ## Why
    
    After turning Fast mode off in the TUI, returning from a side thread
    could make `Fast` appear again in the main chat widget. The opt-out
    itself was still persisted; the display was being rebuilt from stale
    cached `ThreadSessionState` data, which made it look like Fast had been
    re-enabled.
    
    Fixes #23104.
    
    ## What changed
    
    - Keep the active thread's cached `service_tier` in sync whenever the
    user persists a service-tier selection.
    - Update both the primary-thread snapshot and the thread event store so
    restored TUI state reflects the current tier.
    - Add a focused regression test for clearing a cached Fast tier.
    
    ## Manual repro
    
    1. Start a TUI session where `Fast` is enabled by default.
    2. Run `/fast` and turn Fast mode off. Confirm `Fast` disappears from
    the chat widget display.
    3. Re-enter thread navigation via either path:
       - Run `/side test`, then return to the main thread.
       - Run `/agent`, enter a child thread, then return to the main thread.
    4. Before this fix, `Fast` reappears in the main chat widget display
    even though the opt-out was already persisted.
    5. After this fix, `Fast` stays cleared.
    
    ## Verification
    
    - `cargo test -p codex-tui
    app::thread_session_state::tests::service_tier_sync_updates_active_cached_session
    -- --exact`
  • [1 of 4] tui: route primary settings writes through app server (#22913)
    ## Why
    The TUI can run against a remote app server, but several high-traffic
    settings still persisted by editing the local config file. That sends
    remote sessions' preference writes to the wrong machine and lets local
    disk state drift from the app-server-owned config.
    
    This is **[1 of 4]** in a stacked series that moves TUI-owned config
    mutations onto app-server APIs.
    
    ## What changed
    - Added a small TUI helper for typed app-server config writes.
    - Routed primary interactive preference writes through
    `config/batchWrite`.
    - Preserved existing profile scoping for settings that already support
    `profiles.<profile>.*` overrides.
    
    ## Config keys affected
    - `model`
    - `model_reasoning_effort`
    - `personality`
    - `service_tier`
    - `plan_mode_reasoning_effort`
    - `approvals_reviewer`
    - `notice.fast_default_opt_out`
    - Profile-scoped equivalents under `profiles.<profile>.*`
    
    ## Suggested manual validation
    - Connect the TUI to a remote app server, change `model` and
    `model_reasoning_effort`, reconnect, and confirm the remote config
    retained both values while the local `config.toml` did not change.
    - Change `personality`, `plan_mode_reasoning_effort`, and the explicit
    auto-review selection, then reconnect and confirm those choices persist
    through the app server.
    - Clear the service tier back to default and confirm `service_tier` is
    cleared while `notice.fast_default_opt_out = true` is persisted
    remotely.
    - Repeat one setting change with an active profile and confirm the write
    lands under `profiles.<profile>.*`.
    
    ## Stack
    1. [#22913](https://github.com/openai/codex/pull/22913) `[1 of 4]`
    primary settings writes
    2. [#22914](https://github.com/openai/codex/pull/22914) `[2 of 4]` app
    and skill enablement
    3. [#22915](https://github.com/openai/codex/pull/22915) `[3 of 4]`
    feature and memory toggles
    4. [#22916](https://github.com/openai/codex/pull/22916) `[4 of 4]`
    startup and onboarding bookkeeping
  • core: set permission profiles from snapshots (#22920)
    ## Why
    
    #22891 moved the TUI turn-command path to pass `ActivePermissionProfile`
    instead of the full `PermissionProfile`, but the remaining
    config/session bridge still accepted the concrete `PermissionProfile`
    and active profile id as separate arguments. That shape made it too easy
    for future callers to update the concrete profile and active profile id
    out of sync.
    
    This PR makes the trusted session snapshot path pass one coherent value
    into `Permissions`, while keeping `requirements.toml` enforcement owned
    by the existing constrained permission state.
    
    ## What Changed
    
    - Added `PermissionProfileSnapshot` as the public snapshot value for
    trusted session/config synchronization.
    - Changed `Permissions::set_permission_profile_from_session_snapshot()`
    and `replace_permission_profile_from_session_snapshot()` to take a
    `PermissionProfileSnapshot`.
    - Updated the replacement path to derive its constrained
    `PermissionProfile` from the snapshot, so callers cannot pass a separate
    profile that disagrees with the snapshot.
    - Removed the internal tuple-style
    `PermissionProfileState::set_active_permission_profile()` mutation path.
    - Updated core session projection and TUI call sites to construct
    explicit legacy or active snapshots.
    - Documented the snapshot constructors so legacy use and id/profile
    mismatch hazards are called out at the API boundary.
    - Added a focused config test that verifies snapshot updates still
    respect existing permission constraints.
    
    ## How To Review
    
    1. Start with `codex-rs/core/src/config/resolved_permission_profile.rs`;
    `PermissionProfileSnapshot` is the public wrapper, while
    `ResolvedPermissionProfile` stays internal.
    2. Check `codex-rs/core/src/config/mod.rs` to confirm both
    session-snapshot setters validate through `PermissionProfileState` and
    no longer accept loose profile/id pairs.
    3. Skim `codex-rs/core/src/session/session.rs` for the session
    projection path; it now builds the snapshot before installing it.
    4. Skim the TUI changes as call-site migration from loose argument pairs
    to explicit snapshot construction.
    
    ## Verification
    
    - `cargo test -p codex-core
    permission_snapshot_setter_preserves_permission_constraints`
    - `cargo test -p codex-tui status_permissions_`
    - `cargo test -p codex-tui
    session_configured_preserves_profile_workspace_roots`
    - `just fix -p codex-core -p codex-tui`
  • app-server-protocol: remove PermissionProfile from API (#22924)
    ## Why
    
    The app server API should expose permission profile identity, not the
    lower-level runtime permission model. `PermissionProfile` is the
    compiled sandbox/network representation that the server uses internally;
    exposing it through app-server-protocol forces clients to understand
    details that should remain implementation-level.
    
    The API boundary should prefer `ActivePermissionProfile`: a stable
    profile id, plus future parent-profile metadata, that clients can pass
    back when they want to select the same active permissions. This also
    avoids schema generation collisions between the app-server v2 API type
    space and the core protocol model.
    
    Incidentally, while PR makes a number of changes to `command/exec`, note
    that we are hoping to deprecate this API in favor of `process/spawn`, so
    we don't need to be too finicky about these changes.
    
    ## What Changed
    
    - Removed `PermissionProfile` from the app-server-protocol API surface,
    including generated schema and TypeScript exports.
    - Changed `CommandExecParams.permissionProfile` to
    `ActivePermissionProfile`.
    - Resolve command exec profile ids through `ConfigManager` for the
    command cwd, matching turn override selection semantics.
    - Updated downstream TUI tests/helpers to use core permission types
    directly instead of app-server-protocol `PermissionProfile` shims.
  • tui: pass active permission profiles through app commands (#22891)
    ## Why
    
    This continues the permissions migration by keeping the TUI command
    boundary aligned with the app-server protocol direction from #22795:
    callers should select a permission profile by id instead of passing a
    concrete `PermissionProfile` value around as the turn configuration.
    
    `AppCommand` is internal to the TUI, but it is the path that eventually
    becomes `thread/turn/start`, so carrying concrete profile details there
    made it too easy for UI code to keep relying on the old whole-profile
    replacement model.
    
    ## What changed
    
    - `AppCommand::UserTurn` and `AppCommand::OverrideTurnContext` now carry
    `Option<ActivePermissionProfile>` instead of `PermissionProfile`.
    - Composer submissions copy the active permission profile id from the
    current session snapshot; legacy snapshots intentionally submit no
    active profile id.
    - Permission preset UI events now carry only the active built-in profile
    id. The app derives the concrete built-in `PermissionProfile` internally
    only when updating its local config/status snapshot.
    - Permission presets expose their built-in active profile id, and preset
    selection preserves that id in both the immediate turn override and the
    local TUI config snapshot.
    - Turn routing sends `TurnPermissionsOverride::ActiveProfile` when an
    active id is present, and only falls back to the legacy sandbox
    projection for the remaining runtime override path.
    
    ## How to review
    
    Start with `codex-rs/tui/src/app_command.rs` to verify the command shape
    no longer exposes `PermissionProfile`.
    
    Then read `codex-rs/tui/src/app/thread_routing.rs` to verify the
    app-server turn-start conversion: active ids go through as ids, while
    the legacy sandbox fallback is still constrained to the existing runtime
    override case.
    
    Finally, check `codex-rs/tui/src/chatwidget/permission_popups.rs`,
    `codex-rs/tui/src/app/event_dispatch.rs`,
    `codex-rs/tui/src/app/config_persistence.rs`, and
    `codex-rs/utils/approval-presets/src/lib.rs` to see how preset
    selections stay id-only across TUI events while the local display/config
    mirror still gets a concrete built-in profile.
    
    ## Verification
    
    Latest local verification after the id-only `AppEvent` cleanup:
    
    - `cargo check -p codex-tui --tests`
    - `cargo test -p codex-tui
    permissions_selection_sends_approvals_reviewer_in_override_turn_context`
    - `cargo test -p codex-tui update_feature_flags_enabling_guardian`
    - `cargo test -p codex-utils-approval-presets`
    - `just fmt`
    - `just fix -p codex-tui -p codex-utils-approval-presets`
    
    Earlier in the same PR, before the final event-shape cleanup:
    
    - `cargo test -p codex-tui turn_permissions_`
    - `cargo test -p codex-tui submission_`
    - `cargo test -p codex-tui
    session_configured_syncs_widget_config_permissions_and_cwd`
    - `RUST_MIN_STACK=16777216 cargo test -p codex-tui`
  • Preserve image detail in app-server inputs (#20693)
    ## Summary
    
    - Add optional image detail to user image inputs across core, app-server
    v2, thread history/event mapping, and the generated app-server
    schemas/types.
    - Preserve requested detail when serializing Responses image inputs:
    omitted detail stays on the existing `high` default, while explicit
    `original` keeps local images on the original-resolution path.
    - Support `high`/`original` consistently for tool image outputs,
    including MCP `codex/imageDetail`, code-mode image helpers, and
    `view_image`.
  • app-server: stop returning thread permission profiles (#22792)
    ## Why
    
    The app-server thread lifecycle API should no longer expose the full
    `PermissionProfile` value. After the permissions-profile migration,
    clients should round-trip only the active profile identity through
    `activePermissionProfile` and `permissions` when that identity is known.
    
    The full profile is server-side config. Treating a response-derived
    legacy sandbox projection as a new local profile can lose named-profile
    restrictions and accidentally widen permissions on the next turn. The
    legacy `sandbox` response field remains only as the
    compatibility/display fallback.
    
    ## What Changed
    
    - Removed `permissionProfile` from `ThreadStartResponse`,
    `ThreadResumeResponse`, and `ThreadForkResponse`.
    - Stopped populating that field in app-server thread start/resume/fork
    responses.
    - Updated embedded exec/TUI response mapping to derive display
    permission state from local config or the legacy sandbox fallback
    instead of a response profile value.
    - Added a TUI turn override shape that distinguishes preserving server
    permissions, selecting an active profile id, and sending a legacy
    sandbox for an explicit local override.
    - Preserved remote app-server permissions across turns by sending
    `permissions` only when an `activePermissionProfile` id is known, and
    otherwise sending no sandbox override unless the user selected a local
    override.
    - Kept embedded `thread/resume` hydration server-authored when
    `activePermissionProfile` is absent, which matches the live-thread
    attach path where the server ignores requested overrides.
    - Updated the app-server README to remove the obsolete lifecycle
    response `permissionProfile` reference. The remaining
    `permissionProfile` README references are request-side permission
    overrides.
    - Regenerated app-server JSON schema and TypeScript fixtures.
    - Kept the generated typed response enum exempt from
    `large_enum_variant`, matching the existing payload enum exemption after
    the lifecycle response variants shrank.
    
    ## How To Review
    
    Start with `codex-rs/app-server-protocol/src/protocol/v2/thread.rs` to
    confirm the response shape, then check the response construction in
    `codex-rs/app-server/src/request_processors`. The generated schema and
    TypeScript fixture changes are mechanical follow-through from the
    protocol removal.
    
    The TUI behavior is the delicate part: review
    `codex-rs/tui/src/app_server_session.rs` for response hydration and
    turn-start override projection, then
    `codex-rs/tui/src/app/thread_routing.rs` for the decision about whether
    the next turn should preserve the server snapshot, send an active
    profile id, or send a legacy sandbox for an explicit local override.
    
    ## Verification
    
    - `just write-app-server-schema`
    - `cargo test -p codex-app-server-protocol
    thread_lifecycle_responses_default_missing_optional_fields`
    - `cargo test -p codex-exec
    session_configured_from_thread_response_uses_permission_profile_from_config`
    - `cargo test -p codex-tui --lib thread_response`
    - `cargo test -p codex-tui turn_permissions_`
    - `cargo test -p codex-tui
    resume_response_restores_turns_from_thread_items`
    - `cargo test -p codex-analytics
    track_response_only_enqueues_analytics_relevant_responses`
    - `just fix -p codex-analytics`
    - `just fix -p codex-app-server-protocol`
    - `just fix -p codex-tui`
    - `just argument-comment-lint`
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/22792).
    * #22795
    * __->__ #22792
  • tui: split remaining composer draft and footer state (#22656)
    ## Why
    
    [#22581](https://github.com/openai/codex/pull/22581) started separating
    the chat composer’s responsibilities, but `ChatComposer` still owned the
    remaining editable draft state alongside footer/status presentation
    state. This follow-up makes those ownership lines explicit so future
    composer changes have a smaller blast radius and `BottomPane` does not
    need to keep exposing scattered draft getters.
    
    This is just a refactor. No functional or behavioral changes are
    intended.
    
    ## What changed
    
    - Move the remaining editable composer state into
    `bottom_pane/chat_composer/draft_state.rs`.
    - Move footer and status-row presentation state into
    `bottom_pane/chat_composer/footer_state.rs`.
    - Add an internal `ComposerDraftSnapshot` for restore flows, replacing
    several ad hoc `BottomPane` pass-through reads.
    - Rewire the related history-search and thread-input restore paths to
    use the extracted state.
    
    ## Verification
    
    - `RUST_MIN_STACK=8388608 cargo test -p codex-tui`
    - `cargo insta pending-snapshots`
  • tui/exec: show effective workspace roots in summaries (#22612)
    ## Why
    
    This PR builds on [#22611](https://github.com/openai/codex/pull/22611).
    
    After `runtimeWorkspaceRoots` moved onto thread state, the user-facing
    summaries were still inconsistent about which roots they showed. In
    particular, `/status` and the exec startup summary could under-report
    extra workspace roots from `--add-dir` or from profile-defined
    `workspace_roots`, which made the new model look incorrect even when the
    permissions themselves were right.
    
    ## What Changed
    
    - switched the TUI status surfaces to summarize against
    `Config::effective_workspace_roots()`
    - updated the exec human-output summary to render from the effective
    permission profile instead of the raw constrained profile
    - added focused regressions for both the TUI and exec code paths so
    extra workspace roots stay visible in user-facing summaries
    
    ## Verification
    
    Targeted coverage for this follow-up lives in:
    - `codex-rs/tui/src/status/tests.rs`
    - `codex-rs/exec/src/event_processor_with_human_output_tests.rs`
    
    The added regressions verify that:
    - status output includes profile-defined workspace roots in the
    effective permissions summary
    - exec startup output includes runtime workspace roots instead of
    collapsing back to `cwd` only
  • app-server: use permission ids and runtime workspace roots (#22611)
    ## Why
    
    This PR builds on [#22610](https://github.com/openai/codex/pull/22610)
    and is the app-server side of the migration from mutable per-turn
    `SandboxPolicy` replacement toward selecting immutable permission
    profiles by id plus mutable runtime workspace roots.
    
    Once permission profiles can carry their own immutable
    `workspace_roots`, app-server no longer needs to mutate the selected
    `PermissionProfile` just to represent thread-specific filesystem
    context. The mutable part now lives on the thread as explicit
    `runtimeWorkspaceRoots`, while `:workspace_roots` remains symbolic until
    the sandbox is realized for a turn.
    
    ## What Changed
    
    - Replaced the v2 permission-selection wrapper surface with plain
    profile ids for `thread/start`, `thread/resume`, `thread/fork`, and
    `turn/start`.
    - Removed the API surface for profile modifications
    (`PermissionProfileSelectionParams`,
    `PermissionProfileModificationParams`,
    `ActivePermissionProfileModification`).
    - Added experimental `runtimeWorkspaceRoots` fields to the thread
    lifecycle and turn-start APIs.
    - Threaded runtime workspace roots through core session/thread
    snapshots, turn overrides, app-server request handling, and command
    execution permission resolution.
    - Kept session permission state symbolic so later runtime root updates
    and cwd-only implicit-root retargeting rebind `:workspace_roots`
    correctly.
    - Updated the embedded clients just enough to send and restore the new
    thread state.
    - Refreshed the generated schema/TypeScript artifacts and the app-server
    README to match the new contract.
    
    ## Verification
    
    Targeted coverage for this layer lives in:
    
    - `codex-rs/app-server-protocol/src/protocol/v2/tests.rs`
    - `codex-rs/app-server/tests/suite/v2/thread_start.rs`
    - `codex-rs/app-server/tests/suite/v2/thread_resume.rs`
    - `codex-rs/app-server/tests/suite/v2/turn_start.rs`
    - `codex-rs/core/src/session/tests.rs`
    
    The key regression checks exercise that:
    
    - `runtimeWorkspaceRoots` resolve against the effective cwd on thread
    start.
    - Profile-declared workspace roots are excluded from the runtime
    workspace roots returned by app-server.
    - A turn-level runtime workspace-root update persists onto the thread
    and is returned by `thread/resume`.
    - A named permission profile selected on one turn remains symbolic so a
    later runtime-root-only turn update changes the actual sandbox writes.
    - A cwd-only turn update retargets the implicit runtime cwd root while
    preserving additional runtime roots.
    - The protocol fixtures and generated client artifacts stay in sync with
    the string-based permission selection contract.
    
    
    
    
    
    
    
    
    
    
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/22611).
    * #22612
    * __->__ #22611
  • TUI: split history cells into focused modules (#22704)
    ## Why
    
    `codex-rs/tui/src/history_cell.rs` had become the dumping ground for
    transcript rendering: the shared trait, common helpers, and the concrete
    cells for messages, plans, MCP/search, notices, patches, approvals,
    session chrome, and separators all lived together. That made small
    transcript changes require reopening a very large file and made
    ownership less obvious.
    
    ## What changed
    
    - Replaced the monolithic `history_cell.rs` with a `history_cell/`
    module tree organized by concern.
    - Kept the existing `crate::history_cell::*` surface stable through
    re-exports in `history_cell/mod.rs`.
    - Moved the existing render coverage into `history_cell/tests.rs`.
    
    ## Reviewer notes
    
    - This PR is intentionally mechanical in mature — existing code and
    tests moving into files that match their concern.
    - The snapshot files under `codex-rs/tui/src/history_cell/snapshots/`
    moved with the extracted test module. `insta` resolves these unnamed
    snapshots relative to the source file that declares them, so this is
    path churn only; snapshot contents were not updated.
    - The small non-mechanical seam edits are limited to split fallout:
    sibling-module visibility for shared cell containers, moving
    approval-specific exec-snippet helpers beside approvals, fixing the
    separator module path, and keeping a couple of existing test helpers
    reachable after extraction.